A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
392 lines
14 KiB
TypeScript
392 lines
14 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
|
|
/**
|
|
* E2E Tests for /korrektur/archiv
|
|
*
|
|
* Tests the Abitur-Archiv functionality:
|
|
* - Page loads correctly
|
|
* - Search and filters work
|
|
* - Document cards are displayed
|
|
* - Preview modal opens/closes
|
|
* - "Verwenden" modal works
|
|
* - Navigation works
|
|
*/
|
|
|
|
test.describe('Korrektur Archiv', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Navigate to the archiv page
|
|
await page.goto('/korrektur/archiv')
|
|
// Wait for the page to be fully loaded
|
|
await page.waitForLoadState('networkidle')
|
|
})
|
|
|
|
// ==========================================================================
|
|
// PAGE LOAD TESTS
|
|
// ==========================================================================
|
|
|
|
test('page loads with correct title', async ({ page }) => {
|
|
// Check page title/header
|
|
await expect(page.getByRole('heading', { name: 'Abitur-Archiv' })).toBeVisible()
|
|
})
|
|
|
|
test('page shows subtitle', async ({ page }) => {
|
|
await expect(page.getByText('Zentralabitur-Materialien 2021-2025 durchsuchen')).toBeVisible()
|
|
})
|
|
|
|
test('back button is visible', async ({ page }) => {
|
|
// The back arrow button in the header
|
|
const backButton = page.locator('button').filter({ has: page.locator('svg path[d*="15 19l-7-7"]') }).first()
|
|
await expect(backButton).toBeVisible()
|
|
})
|
|
|
|
// ==========================================================================
|
|
// SEARCH TESTS
|
|
// ==========================================================================
|
|
|
|
test('search input is visible and functional', async ({ page }) => {
|
|
const searchInput = page.getByPlaceholder('Thema suchen')
|
|
await expect(searchInput).toBeVisible()
|
|
|
|
// Type a search query
|
|
await searchInput.fill('Kafka')
|
|
await expect(searchInput).toHaveValue('Kafka')
|
|
})
|
|
|
|
test('popular theme tags are displayed', async ({ page }) => {
|
|
// Check that popular theme tags are visible
|
|
await expect(page.getByRole('button', { name: 'Textanalyse' })).toBeVisible()
|
|
await expect(page.getByRole('button', { name: 'Gedichtanalyse' })).toBeVisible()
|
|
await expect(page.getByRole('button', { name: 'Eroerterung' })).toBeVisible()
|
|
})
|
|
|
|
test('clicking theme tag updates search', async ({ page }) => {
|
|
const searchInput = page.getByPlaceholder('Thema suchen')
|
|
const themeTag = page.getByRole('button', { name: 'Textanalyse' })
|
|
|
|
await themeTag.click()
|
|
|
|
// Search input should now have the theme
|
|
await expect(searchInput).toHaveValue('Textanalyse')
|
|
})
|
|
|
|
test('clear search button works', async ({ page }) => {
|
|
const searchInput = page.getByPlaceholder('Thema suchen')
|
|
|
|
// Type something
|
|
await searchInput.fill('Test')
|
|
|
|
// Find and click the clear button (X icon)
|
|
const clearButton = page.locator('button').filter({ has: page.locator('svg path[d*="6 18L18 6"]') }).first()
|
|
await clearButton.click()
|
|
|
|
// Search should be empty
|
|
await expect(searchInput).toHaveValue('')
|
|
})
|
|
|
|
// ==========================================================================
|
|
// FILTER TESTS
|
|
// ==========================================================================
|
|
|
|
test('filter dropdowns are visible', async ({ page }) => {
|
|
// All 5 filters should be present
|
|
await expect(page.getByLabel('Fach')).toBeVisible()
|
|
await expect(page.getByLabel('Jahr')).toBeVisible()
|
|
await expect(page.getByLabel('Bundesland')).toBeVisible()
|
|
await expect(page.getByLabel('Niveau')).toBeVisible()
|
|
await expect(page.getByLabel('Typ')).toBeVisible()
|
|
})
|
|
|
|
test('filter dropdown changes value', async ({ page }) => {
|
|
const fachFilter = page.getByLabel('Fach')
|
|
|
|
// Select a specific value (lowercase as returned by API)
|
|
await fachFilter.selectOption('deutsch')
|
|
await expect(fachFilter).toHaveValue('deutsch')
|
|
})
|
|
|
|
test('filter reset button appears when filters active', async ({ page }) => {
|
|
const fachFilter = page.getByLabel('Fach')
|
|
|
|
// Initially, reset button should not be visible (or show 0)
|
|
const resetButton = page.getByRole('button', { name: /Filter zuruecksetzen/ })
|
|
|
|
// Select a filter (lowercase as returned by API)
|
|
await fachFilter.selectOption('deutsch')
|
|
|
|
// Reset button should now be visible with count
|
|
await expect(resetButton).toBeVisible()
|
|
await expect(resetButton).toContainText('1')
|
|
})
|
|
|
|
test('filter reset button clears all filters', async ({ page }) => {
|
|
const fachFilter = page.getByLabel('Fach')
|
|
const jahrFilter = page.getByLabel('Jahr')
|
|
|
|
// Set multiple filters (lowercase as returned by API)
|
|
await fachFilter.selectOption('deutsch')
|
|
await jahrFilter.selectOption('2024')
|
|
|
|
// Click reset
|
|
const resetButton = page.getByRole('button', { name: /Filter zuruecksetzen/ })
|
|
await resetButton.click()
|
|
|
|
// Filters should be reset
|
|
await expect(fachFilter).toHaveValue('Alle')
|
|
await expect(jahrFilter).toHaveValue('Alle')
|
|
})
|
|
|
|
// ==========================================================================
|
|
// DOCUMENT CARD TESTS
|
|
// ==========================================================================
|
|
|
|
test('document cards are displayed', async ({ page }) => {
|
|
// Wait for cards to load
|
|
await page.waitForTimeout(600) // Wait for animation delay
|
|
|
|
// Should have document cards
|
|
const cards = page.locator('[class*="rounded-3xl"]').filter({ hasText: 'Deutsch' })
|
|
await expect(cards.first()).toBeVisible()
|
|
})
|
|
|
|
test('document card shows Vorschau button', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
const vorschauButton = page.getByRole('button', { name: 'Vorschau' }).first()
|
|
await expect(vorschauButton).toBeVisible()
|
|
})
|
|
|
|
test('document card shows Verwenden button', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
const verwendenButton = page.getByRole('button', { name: 'Verwenden' }).first()
|
|
await expect(verwendenButton).toBeVisible()
|
|
})
|
|
|
|
// ==========================================================================
|
|
// PREVIEW MODAL TESTS
|
|
// ==========================================================================
|
|
|
|
test('clicking Vorschau opens preview modal', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Click first Vorschau button
|
|
const vorschauButton = page.getByRole('button', { name: 'Vorschau' }).first()
|
|
await vorschauButton.click()
|
|
|
|
// Modal should be visible - check for the "Zurueck zum Archiv" button
|
|
await expect(page.getByRole('button', { name: 'Zurueck zum Archiv' })).toBeVisible()
|
|
})
|
|
|
|
test('preview modal shows document details', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Open preview
|
|
await page.getByRole('button', { name: 'Vorschau' }).first().click()
|
|
|
|
// Check for details section - look within the modal (fixed z-50)
|
|
const modal = page.locator('.fixed.inset-0.z-50')
|
|
await expect(modal.getByText('Details')).toBeVisible()
|
|
// Check that the details section has Fach info (using span in modal)
|
|
await expect(modal.locator('span').filter({ hasText: /^Fach$/ })).toBeVisible()
|
|
})
|
|
|
|
test('preview modal has zoom controls', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Open preview
|
|
await page.getByRole('button', { name: 'Vorschau' }).first().click()
|
|
|
|
// Should show zoom percentage
|
|
await expect(page.getByText('100%')).toBeVisible()
|
|
})
|
|
|
|
test('preview modal zoom in works', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Open preview
|
|
await page.getByRole('button', { name: 'Vorschau' }).first().click()
|
|
|
|
// Find zoom in button (+ icon)
|
|
const zoomInButton = page.locator('button').filter({ has: page.locator('svg path[d*="M12 4v16m8-8H4"]') }).first()
|
|
await zoomInButton.click()
|
|
|
|
// Zoom should increase to 125%
|
|
await expect(page.getByText('125%')).toBeVisible()
|
|
})
|
|
|
|
test('preview modal close button works', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Open preview
|
|
await page.getByRole('button', { name: 'Vorschau' }).first().click()
|
|
|
|
// Wait for modal
|
|
await expect(page.getByRole('button', { name: 'Zurueck zum Archiv' })).toBeVisible()
|
|
|
|
// Click close/back button
|
|
await page.getByRole('button', { name: 'Zurueck zum Archiv' }).click()
|
|
|
|
// Modal should be closed - back button should not be visible
|
|
await expect(page.getByRole('button', { name: 'Zurueck zum Archiv' })).not.toBeVisible()
|
|
})
|
|
|
|
test('preview modal backdrop click closes modal', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Open preview
|
|
await page.getByRole('button', { name: 'Vorschau' }).first().click()
|
|
|
|
// Wait for modal
|
|
await expect(page.getByRole('button', { name: 'Zurueck zum Archiv' })).toBeVisible()
|
|
|
|
// Click the backdrop (top-left corner of the page)
|
|
await page.mouse.click(10, 10)
|
|
|
|
// Modal should be closed
|
|
await expect(page.getByRole('button', { name: 'Zurueck zum Archiv' })).not.toBeVisible()
|
|
})
|
|
|
|
test('preview modal Herunterladen button exists', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Open preview
|
|
await page.getByRole('button', { name: 'Vorschau' }).first().click()
|
|
|
|
// Check for download button in sidebar
|
|
await expect(page.getByRole('link', { name: 'Herunterladen' })).toBeVisible()
|
|
})
|
|
|
|
test('preview modal Als Vorlage verwenden button exists', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Open preview
|
|
await page.getByRole('button', { name: 'Vorschau' }).first().click()
|
|
|
|
// Check for "Als Vorlage verwenden" button
|
|
await expect(page.getByRole('button', { name: 'Als Vorlage verwenden' })).toBeVisible()
|
|
})
|
|
|
|
// ==========================================================================
|
|
// CREATE KLAUSUR MODAL TESTS
|
|
// ==========================================================================
|
|
|
|
test('clicking Verwenden opens create modal', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Click first Verwenden button
|
|
await page.getByRole('button', { name: 'Verwenden' }).first().click()
|
|
|
|
// Create modal should be visible
|
|
await expect(page.getByText('Klausur aus Vorlage erstellen')).toBeVisible()
|
|
})
|
|
|
|
test('create modal has pre-filled title', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Click Verwenden
|
|
await page.getByRole('button', { name: 'Verwenden' }).first().click()
|
|
|
|
// Title input should have a value
|
|
const titleInput = page.getByPlaceholder('z.B. Deutsch LK Q4 - Kafka')
|
|
await expect(titleInput).toBeVisible()
|
|
await expect(titleInput).not.toHaveValue('')
|
|
})
|
|
|
|
test('create modal shows template info', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Click Verwenden
|
|
await page.getByRole('button', { name: 'Verwenden' }).first().click()
|
|
|
|
// Should show template details
|
|
await expect(page.getByText('Fach:')).toBeVisible()
|
|
await expect(page.getByText('Jahr:')).toBeVisible()
|
|
await expect(page.getByText('Niveau:')).toBeVisible()
|
|
})
|
|
|
|
test('create modal cancel button closes modal', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Click Verwenden
|
|
await page.getByRole('button', { name: 'Verwenden' }).first().click()
|
|
|
|
// Wait for modal
|
|
await expect(page.getByText('Klausur aus Vorlage erstellen')).toBeVisible()
|
|
|
|
// Click cancel
|
|
await page.getByRole('button', { name: 'Abbrechen' }).click()
|
|
|
|
// Modal should be closed
|
|
await expect(page.getByText('Klausur aus Vorlage erstellen')).not.toBeVisible()
|
|
})
|
|
|
|
test('create modal Klausur erstellen button exists', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Click Verwenden
|
|
await page.getByRole('button', { name: 'Verwenden' }).first().click()
|
|
|
|
// Should have create button
|
|
await expect(page.getByRole('button', { name: 'Klausur erstellen' })).toBeVisible()
|
|
})
|
|
|
|
// ==========================================================================
|
|
// NAVIGATION TESTS
|
|
// ==========================================================================
|
|
|
|
test('back button navigates to /korrektur', async ({ page }) => {
|
|
// Find the back button in the header
|
|
const backButton = page.locator('button').filter({ has: page.locator('svg path[d*="15 19l-7-7"]') }).first()
|
|
await backButton.click()
|
|
|
|
// Should navigate to /korrektur
|
|
await expect(page).toHaveURL(/\/korrektur$/)
|
|
})
|
|
|
|
// ==========================================================================
|
|
// DOCUMENT COUNT TESTS
|
|
// ==========================================================================
|
|
|
|
test('document count is displayed', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Should show document count
|
|
await expect(page.getByText(/\d+ Dokumente/)).toBeVisible()
|
|
})
|
|
|
|
test('filtering updates document count', async ({ page }) => {
|
|
await page.waitForTimeout(600)
|
|
|
|
// Get initial count
|
|
const countText = page.getByText(/\d+ Dokumente/)
|
|
const initialText = await countText.textContent()
|
|
|
|
// Apply a restrictive filter
|
|
const jahrFilter = page.getByLabel('Jahr')
|
|
await jahrFilter.selectOption('2024')
|
|
|
|
// Wait for filter to apply
|
|
await page.waitForTimeout(300)
|
|
|
|
// Count should potentially change (or stay same if all 2024)
|
|
// Just verify the count element still exists
|
|
await expect(page.getByText(/\d+ Dokumente/)).toBeVisible()
|
|
})
|
|
|
|
// ==========================================================================
|
|
// EMPTY STATE TESTS
|
|
// ==========================================================================
|
|
|
|
test('shows empty state when no results', async ({ page }) => {
|
|
// Search for something that won't match
|
|
const searchInput = page.getByPlaceholder('Thema suchen')
|
|
await searchInput.fill('xyznonexistent12345')
|
|
|
|
// Wait for filter
|
|
await page.waitForTimeout(300)
|
|
|
|
// Should show empty state
|
|
await expect(page.getByText('Keine Dokumente gefunden')).toBeVisible()
|
|
})
|
|
})
|