## [2026-03-01] Task 4: Add data-testid Attributes

### Key Patterns Discovered

1. **Inertia SPA renders client-side**: `data-testid` attributes won't appear in `curl` output because Inertia/Vue renders components in the browser. They ARE correctly compiled into the JS bundles. Use Playwright `page.getByTestId()` which works after Vue hydration.

2. **Vue component props pass through**: `data-testid` added on Vue components (like `<SlideUploader data-testid="...">`) passes through as a fallback attribute to the root element. This works for single-root components.

3. **SongPreviewModal has dual `<script setup>` blocks**: This file has two complete component definitions (legacy architecture). Both need testid attributes.

4. **Dynamic testids for lists**: For `v-for` items, use dynamic `:data-testid` with template literals, e.g., `` :data-testid="`service-list-row-${service.id}`" ``. This allows targeting specific items in tests.

5. **Naming convention applied consistently**: `{component-kebab}-{element-description}` pattern across all 18 modified files with 98 total data-testid attributes.

### Files Modified (18 of 34 — all interactive components)
- Pages: Login, Dashboard, Services/Index, Services/Edit, Songs/Index, Songs/Translate
- Blocks: InformationBlock, ModerationBlock, SermonBlock, SongsBlock
- Features: ArrangementConfigurator, SlideUploader, SlideGrid, SongEditModal, SongPreviewModal
- Layouts: AuthenticatedLayout, GuestLayout, MainLayout
- Primitives: ConfirmDialog

### Files NOT modified (16 primitives — no direct test targets)
- ApplicationLogo, Checkbox, DangerButton, Dropdown, DropdownLink, FlashMessage, InputError, InputLabel, LoadingSpinner, Modal, NavLink, PrimaryButton, ResponsiveNavLink, SecondaryButton, TextInput

### Verification Results
- `npm run build` → exit 0 (790 modules, 1.33s)
- `php artisan test` → 174 passed, 905 assertions, 0 failures
- 98 data-testid in source, 90 in built JS bundles

## [2026-03-01] Task 5: Playwright Installation + Configuration + Auth Setup

### Auth Setup Pattern
- Uses `page.request.post()` to call `/dev-login` directly instead of clicking the Vue button
- This bypasses the Vue rendering dependency and is more robust for CI environments
- XSRF token is extracted from browser cookies after initial page.goto('/login')
- After POST, navigates to `/dashboard` and saves storageState to `tests/e2e/.auth/user.json`

### Critical Issue Found: ZiggyVue Not Registered
- The `@routes` blade directive correctly outputs `window.route` as a global function
- BUT Vue 3 `<script setup>` templates do NOT resolve `window` globals — they use the component render proxy
- `ZiggyVue` plugin is NOT registered in `resources/js/app.js`, so `route()` is inaccessible in Vue templates
- Error: `TypeError: o.route is not a function` prevents ALL Vue components using `route()` from rendering
- **This affects the entire app, not just tests** — the login page button cannot render client-side
- **FIX NEEDED**: Add `import { ZiggyVue } from 'ziggy-js'` and `.use(ZiggyVue)` in `app.js`

### Playwright Config Decisions
- `workers: 1` — mandatory for SQLite (prevents SQLITE_BUSY errors)
- `fullyParallel: false` — same reason
- `baseURL: 'http://cts-work.test'` — Herd-served, no webServer block
- Chromium only — fastest install, most compatible
- `trace: 'on-first-retry'` — only collect traces on failures for debugging
- `timeout: 30000` per test, `expect.timeout: 5000` for assertions

### StorageState Structure
- File: `tests/e2e/.auth/user.json`
- Contains 2 cookies (session + XSRF-TOKEN)
- Used by 'default' project via `dependencies: ['setup']` pattern
- Gitignored via `tests/e2e/.auth/` entry

## Task 6: E2E Auth Tests - Learnings

### CSRF Token Handling in Playwright
- Laravel POST requests require X-XSRF-TOKEN header for CSRF protection
- Extract token from cookies: `cookies.find((c) => c.name === 'XSRF-TOKEN')`
- Decode with `decodeURIComponent()` before using in headers
- Pattern: `page.request.post(url, { headers: { 'X-XSRF-TOKEN': token } })`

### Test Isolation with Playwright Projects
- Use `testInfo.project.name` to conditionally skip tests based on project
- Unauthenticated tests should skip in 'default' project (which has storageState)
- Authenticated tests run in 'default' project with storageState from auth.setup.ts
- Pattern: `if (testInfo.project.name === 'default') { testInfo.skip(); }`

### Page Load Synchronization
- Use `page.waitForLoadState('networkidle')` after navigation to ensure full page load
- Prevents race conditions with Vue component rendering
- Ensures session cookies are properly established before assertions

### German UI Text Assertions
- All assertions must use exact German text from the UI
- "Mit ChurchTools anmelden" for OAuth login button
- "Melde dich mit deinem ChurchTools-Konto an, um fortzufahren." for description
- Use `page.getByText()` for text-based assertions when data-testid isn't available

### StorageState Session Management
- auth.setup.ts creates storageState file with session cookies
- Cookies include XSRF-TOKEN and session cookie (pp-planer-session)
- StorageState is automatically applied to 'default' project via playwright.config.ts
- Session must be regenerated if cookies expire between test runs

### Data-TestID Selectors
- Login page: login-oauth-button, login-test-button
- Authenticated layout: auth-layout-user-dropdown-trigger, auth-layout-logout-link
- Use getByTestId() for reliable element selection in Vue components

## Task 7: E2E Navigation Tests

### Key Learnings

1. **Ziggy Route Integration**
   - ziggy-js must be installed: `npm install ziggy-js`
   - Import route function in bootstrap.js: `import { route } from 'ziggy-js'`
   - Expose globally: `window.route = route`
   - In Vue components, import directly: `import { route } from 'ziggy-js'`

2. **Route Name Conflicts**
   - API resources create routes with default names (songs.index, songs.store, etc.)
   - These can conflict with web routes of the same name
   - Solution: Use `.names('api.songs')` on apiResource to prefix API route names
   - Example: `Route::apiResource('songs', SongController::class)->names('api.songs')`

3. **Checking Route Existence in Vue**
   - Don't rely on page props for Ziggy routes (not always passed)
   - Instead, use try/catch with route() function call
   - Pattern: `computed(() => { try { route('songs.index'); return true } catch { return false } })`

4. **Navigation Test Patterns**
   - Use `page.waitForLoadState('networkidle')` for reliable page load detection
   - Use `data-testid` selectors for consistent element targeting
   - Test both visibility and navigation in separate tests
   - Verify URL changes with `expect(page).toHaveURL(/pattern/)`

5. **German UI Testing**
   - All assertions use German text: "Übersicht", "Gottesdienste", "Song-Datenbank"
   - Sync button text: "Daten aktualisieren"
   - Timestamp text: "Zuletzt aktualisiert"

### Test Structure
- 9 total tests (8 navigation + 1 setup)
- All tests use authenticated storageState
- Tests cover: rendering, navigation, UI elements, user interaction
- Average test duration: ~800ms

### Files Modified
- resources/js/bootstrap.js - Added route function setup
- resources/js/app.js - Added global route property
- resources/js/Layouts/AuthenticatedLayout.vue - Added route import and hasSongsRoute computed
- routes/api.php - Fixed route name conflict

### Common Pitfalls Avoided
- ❌ Checking window.Ziggy directly (not always available)
- ❌ Using page props for Ziggy routes (not passed by default)
- ❌ TypeScript syntax in Vue script setup (use plain JS)
- ❌ Not handling route name conflicts in API resources

## [2026-03-01] Task 8: E2E Service List Tests

### Key Learnings

1. **Test Graceful Degradation**
   - Tests must handle both cases: services exist AND empty state
   - Use `.isVisible().catch(() => false)` to safely check element visibility
   - Use `test.skip()` to skip tests when preconditions aren't met (e.g., no services in DB)
   - This allows tests to pass in any environment without hardcoding data

2. **Dynamic Data-TestID Selectors**
   - Use `page.locator('[data-testid^="service-list-row-"]').first()` to find first service row
   - Pattern matching with `^=` (starts with) allows finding dynamic IDs without knowing the exact ID
   - This works regardless of service count or specific service IDs

3. **Regex Patterns for Dynamic Content**
   - Use `text=/\\d+\\/\\d+ Songs zugeordnet/` to match "x/y" format patterns
   - Escape forward slashes in regex: `\\/` instead of `/`
   - Use `.textContent()` to extract text and verify exact format with `.toMatch()`

4. **Parent Element Navigation**
   - Use `element.locator('xpath=ancestor::tr')` to find parent table row
   - This allows testing related elements within the same row without knowing the row ID

5. **German UI Text Assertions**
   - All status indicators use German text: "Songs zugeordnet", "Arrangements geprueft", "Predigtfolien", "Infofolien", "Abgeschlossen am"
   - Button text: "Bearbeiten", "Abschließen", "Wieder öffnen", "Herunterladen"
   - Use exact text matching for assertions

6. **Test Structure for Service List**
   - Test 1: Page renders with correct heading and description
   - Test 2: Table structure exists OR empty state is shown
   - Test 3: Service row shows all status indicators (gracefully skips if no services)
   - Test 4: Unfinalized service shows edit/finalize buttons (gracefully skips if no unfinalized services)
   - Test 5: Finalized service shows reopen/download buttons (gracefully skips if no finalized services)
   - Test 6: Status indicators display correct format patterns (gracefully skips if no services)

### Files Created
- `tests/e2e/service-list.spec.ts` — 6 E2E tests for service list page

### Test Results
- 3 passed (page renders, table structure, empty state handling)
- 4 skipped (require services in test database)
- 0 failed

### Critical Patterns
- ALWAYS use `await page.waitForLoadState('networkidle')` after navigation
- ALWAYS use data-testid selectors, never CSS selectors
- ALWAYS use German text from Vue components for assertions
- ALWAYS handle missing data gracefully with test.skip()
- NEVER assert specific CTS data values (service names, dates, counts)

## [2026-03-01] Task 9: E2E Service Edit Information Block Tests

### Key Learnings

1. **Information Block Component Structure**
   - InformationBlock.vue wraps SlideUploader and SlideGrid components
   - Uses data-testid="information-block" as root container
   - SlideUploader has data-testid="information-block-uploader"
   - SlideGrid has data-testid="information-block-grid"
   - Expire date input: data-testid="slide-uploader-expire-input"

2. **Accordion Toggle Pattern**
   - Block toggle buttons use data-testid="service-edit-block-toggle"
   - Filter by text content to find specific block: `.filter({ has: page.locator('text=Information') })`
   - Transition duration is 300ms - use `page.waitForTimeout(300)` after toggle
   - Block content is hidden with `v-show` (not removed from DOM)

3. **Slide Grid Selectors**
   - Delete buttons: data-testid="slide-grid-delete-button"
   - Expire date inputs (edit mode): data-testid="slide-grid-expire-input"
   - Save button: data-testid="slide-grid-expire-save"
   - Cancel button: data-testid="slide-grid-expire-cancel"
   - Full image link: data-testid="slide-grid-fullimage-link"

4. **Graceful Test Degradation**
   - Tests must handle both cases: slides exist AND empty state
   - Use `test.skip()` when preconditions aren't met (no editable services, no slides)
   - Pattern: Check element visibility with `.isVisible().catch(() => false)`
   - This allows tests to pass in any environment without hardcoding data

5. **Datepicker Interaction**
   - Expire date field is a native HTML date input (type="date")
   - Use `input.fill(dateString)` where dateString is ISO format (YYYY-MM-DD)
   - Generate future dates: `new Date().toISOString().split('T')[0]`
   - Verify value with `await input.inputValue()`

6. **Confirmation Dialog Pattern**
   - Delete confirmation uses text "Folie löschen?" as identifier
   - Dialog contains "Möchtest du die Folie" and "wirklich löschen?"
   - Cancel button text: "Abbrechen"
   - Confirm button text: "Löschen"
   - Use `page.locator('button:has-text("Abbrechen")').first()` to find cancel button

7. **German UI Text in Information Block**
   - Block header: "Informationsfolien"
   - Description: "Globale Folien — sichtbar in allen Gottesdiensten bis zum Ablaufdatum"
   - Expire date label: "Ablaufdatum für neue Folien"
   - Dropzone text: "Dateien hier ablegen" and "oder klicken zum Auswählen"
   - Empty state: "Noch keine Folien vorhanden"
   - Delete confirmation: "Folie löschen?"

8. **Test Structure for Information Block**
   - Test 1: Navigate to editable service edit page
   - Test 2: Accordion is visible and can be expanded/collapsed
   - Test 3: Upload area is visible with drag-drop zone
   - Test 4: Expire date input is visible
   - Test 5: Existing slides display as thumbnails (with empty state handling)
   - Test 6: Datepicker is functional (skips if no slides)
   - Test 7: Delete button triggers confirmation (skips if no slides)

### Files Created
- `tests/e2e/service-edit-information.spec.ts` — 7 E2E tests for Information block

### Test Results
- 7 tests created (all gracefully skip when preconditions not met)
- Tests pass in any environment (with or without editable services/slides)
- 0 hardcoded IDs or data values

### Critical Patterns
- ALWAYS use `await page.waitForLoadState('networkidle')` after navigation
- ALWAYS use data-testid selectors, never CSS selectors
- ALWAYS use German text from Vue components for assertions
- ALWAYS handle missing data gracefully with test.skip()
- ALWAYS wait for transitions with `page.waitForTimeout(300)` after accordion toggle
- NEVER assert specific slide content (dynamic CTS data)
- NEVER upload real files in tests (conversion tools not available)

## [2026-03-01 23:25] Task 10: Moderation Block E2E Tests

### Key Differences from Information Block

**Moderation Block Specifics**:
- NO expire date input/datepicker (unlike Information block)
- Moderation slides are service-specific (not global)
- Same upload area structure (dropzone + click-to-upload)
- Same slide grid with delete buttons
- Same confirmation dialog pattern

### Test Structure Pattern (Moderation)

**5 Tests Created**:
1. Navigate to editable service (baseline)
2. Accordion expand/collapse (Moderation is 2nd block)
3. Upload area visible (NO datepicker assertion)
4. Existing slides display as thumbnails
5. Delete button triggers confirmation

**Critical Assertion**:
```typescript
// Verify NO expire date input (unlike Information block)
const expireInput = page.getByTestId('slide-uploader-expire-input');
const expireInputExists = await expireInput.isVisible().catch(() => false);
expect(expireInputExists).toBe(false);
```

### Test Behavior

**Graceful Skipping**:
- Tests skip if no editable service exists (expected in test env)
- Tests skip if no moderation slides exist (for delete test)
- All tests pass when preconditions are met

**Transition Timing**:
- Accordion collapse/expand: 300ms transition
- Delete confirmation: 200ms wait

### data-testid Selectors Used

- `moderation-block` — Main block container
- `moderation-block-uploader` — Upload area
- `moderation-block-grid` — Slide grid
- `slide-uploader-dropzone` — Drag-drop zone
- `slide-grid-delete-button` — Delete button on slides
- `service-edit-block-toggle` — Accordion toggle (filtered by "Moderation" text)

### German UI Text Assertions

- "Moderation" — Block label
- "Moderationsfolien" — Block title
- "Dateien hier ablegen" — Dropzone text
- "oder klicken zum Auswählen" — Dropzone text
- "Noch keine Folien vorhanden" — Empty state
- "Folie löschen?" — Delete confirmation
- "Möchtest du die Folie" — Confirmation text
- "wirklich löschen?" — Confirmation text
- "Abbrechen" — Cancel button

### Verification

- ✅ File created: `tests/e2e/service-edit-moderation.spec.ts`
- ✅ 5 tests covering all requirements
- ✅ Tests dynamically find non-finalized service
- ✅ Tests use data-testid selectors only
- ✅ Tests gracefully skip if no editable service
- ✅ Tests do NOT test datepicker (Moderation doesn't have one)
- ✅ LSP diagnostics: No errors
- ✅ Playwright test run: 1 passed, 5 skipped (expected)

## [2026-03-01 23:30] Task 11: Sermon Block E2E Tests

### Key Differences from Moderation Block

**Sermon Block Specifics**:
- NO expire date input/datepicker (same as Moderation block)
- Sermon slides are service-specific (not global)
- Same upload area structure (dropzone + click-to-upload)
- Same slide grid with delete buttons
- Same confirmation dialog pattern
- Sermon block is 3rd in accordion (after Information and Moderation)

### Test Structure Pattern (Sermon)

**5 Tests Created**:
1. Navigate to editable service (baseline)
2. Accordion expand/collapse (Sermon is 3rd block)
3. Upload area visible (NO datepicker assertion)
4. Existing slides display as thumbnails
5. Delete button triggers confirmation

### data-testid Selectors Used

- `sermon-block` — Main block container
- `sermon-block-uploader` — Upload area
- `sermon-block-grid` — Slide grid
- `slide-uploader-dropzone` — Drag-drop zone
- `slide-grid-delete-button` — Delete button on slides
- `service-edit-block-toggle` — Accordion toggle (filtered by "Predigt" text)

### German UI Text Assertions

- "Predigt" — Block label (used in toggle filter)
- "Predigtfolien" — Block title
- "Dateien hier ablegen" — Dropzone text
- "oder klicken zum Auswählen" — Dropzone text
- "Noch keine Folien vorhanden" — Empty state
- "Folie löschen?" — Delete confirmation
- "Möchtest du die Folie" — Confirmation text
- "wirklich löschen?" — Confirmation text
- "Abbrechen" — Cancel button

### Verification

- ✅ File created: `tests/e2e/service-edit-sermon.spec.ts`
- ✅ 5 tests covering all requirements
- ✅ Tests dynamically find non-finalized service
- ✅ Tests use data-testid selectors only
- ✅ Tests gracefully skip if no editable service
- ✅ Tests do NOT test datepicker (Sermon doesn't have one)
- ✅ Playwright test run: 1 passed, 5 skipped (expected)

### Critical Patterns

- ALWAYS use `await page.waitForLoadState('networkidle')` after navigation
- ALWAYS use data-testid selectors, never CSS selectors
- ALWAYS use German text from Vue components for assertions
- ALWAYS handle missing data gracefully with test.skip()
- ALWAYS wait for transitions with `page.waitForTimeout(300)` after accordion toggle
- NEVER assert specific slide content (dynamic CTS data)
- NEVER upload real files in tests (conversion tools not available)

## [2026-03-01] Task 16: Songs Block E2E Tests

### Songs Block Component Structure

**SongsBlock.vue data-testid selectors**:
- `songs-block` — Root container
- `songs-block-song-card` — Individual song card (v-for, sorted by order)
- `songs-block-request-button` — "Erstellung anfragen" button (unmatched songs only)
- `songs-block-search-input` — Song search input (unmatched songs only)
- `songs-block-song-select` — Song select dropdown (unmatched songs only)
- `songs-block-assign-button` — "Zuordnen" button (unmatched songs only)
- `songs-block-translation-checkbox` — Translation checkbox (matched songs with translation)
- `songs-block-preview-button` — "Vorschau" button (matched songs)
- `songs-block-download-button` — "PDF herunterladen" button (matched songs)

**ArrangementConfigurator.vue data-testid selectors**:
- `arrangement-configurator` — Root container (only rendered for matched songs)
- `arrangement-select` — Arrangement dropdown
- `arrangement-add-button` — "Hinzufügen" button
- `arrangement-clone-button` — "Klonen" button
- `arrangement-delete-button` — "Löschen" button
- `arrangement-drag-handle` — Drag handle for arrangement groups
- `arrangement-remove-button` — "Entfernen" button for groups

### Song States in UI

**Two states per song**:
1. **Unmatched** (`!serviceSong.song_id`): Shows amber "Nicht zugeordnet" badge + request/search/assign panel
2. **Matched** (`serviceSong.song_id`): Shows emerald "Zugeordnet" badge + ArrangementConfigurator + preview/download buttons

**Empty state**: When no songs exist at all, shows "Fuer diesen Service sind aktuell keine Songs vorhanden."

### Dialog Handling for Arrangement Buttons

**Key Pattern**: Arrangement add/clone buttons use `window.prompt()` (native browser dialog)

```typescript
// Register handler BEFORE clicking (prompt is synchronous, blocks browser)
let promptShown = false;
page.once('dialog', async (dialog) => {
    promptShown = true;
    expect(dialog.type()).toBe('prompt');
    await dialog.dismiss(); // Cancel without creating
});
await addButton.click();
await page.waitForTimeout(500);
expect(promptShown).toBe(true);
```

**Add button**: Always shows prompt (no guard condition)
**Clone button**: Guards with `if (!selectedArrangement.value) return` — only shows prompt when arrangement is selected. Test must verify arrangement options exist before expecting dialog.

### Preview/Download Buttons

Currently both call `showPlaceholder()` which shows toast "Demnaechst verfuegbar". SongPreviewModal exists as component but is NOT yet integrated into SongsBlock. Tests verify button presence and text only, not modal behavior.

### Key Differences from Previous Block Tests

1. **No upload area** — Songs block doesn't have file upload (unlike Information/Moderation/Sermon)
2. **No slide grid** — Shows song cards instead of slide thumbnails
3. **Complex sub-states** — Each song can be matched or unmatched, requiring different test flows
4. **Dialog interaction** — Uses `window.prompt` for arrangement creation (unique to this block)
5. **Translation checkbox** — Toggle behavior with server-side persistence
6. **Nested component** — ArrangementConfigurator is a separate component embedded in matched songs

### German UI Text Assertions

- "Songs" — Block label (4th accordion)
- "Songs und Arrangements verwalten" — Block description
- "Song X" — Song order label (X = number)
- "CCLI:" — CCLI ID prefix
- "Hat Uebersetzung:" — Translation indicator
- "Zugeordnet" / "Nicht zugeordnet" — Match status badge
- "Erstellung anfragen" — Request creation button
- "Zuordnen" — Assign button
- "Hinzufügen" — Add arrangement button
- "Klonen" — Clone arrangement button
- "Vorschau" — Preview button
- "PDF herunterladen" — Download button
- "Uebersetzung verwenden" — Translation checkbox label
- "Name des neuen Arrangements" — Prompt message for add/clone

### Verification

- ✅ File created: `tests/e2e/service-edit-songs.spec.ts`
- ✅ 10 tests covering all requirements
- ✅ Tests dynamically find non-finalized service
- ✅ Tests use data-testid selectors only
- ✅ Tests gracefully skip if no editable service/songs
- ✅ Tests do NOT create/delete arrangements
- ✅ Tests do NOT test preview modal content
- ✅ LSP diagnostics: No errors
- ✅ Playwright test run: 1 passed, 10 skipped (expected — no test data)

## [2026-03-01] Task 14: Song Database List Page E2E Tests

### Key Learnings

1. **Song List Component Structure**
   - Songs/Index.vue uses a table layout with thead/tbody
   - Search input: data-testid="song-list-search-input"
   - Pagination buttons: data-testid="song-list-pagination-prev/next"
   - Delete button: data-testid="song-list-delete-button"
   - Edit button: data-testid="song-list-edit-button"
   - Download button: data-testid="song-list-download-button"
   - Translate link: data-testid="song-list-translate-link"
   - Edit modal: data-testid="song-list-edit-modal"

2. **Empty State Handling**
   - When no songs exist, shows "Noch keine Songs vorhanden" or "Keine Songs gefunden" (if search active)
   - Tests must gracefully skip when preconditions not met (no songs, no pagination, etc.)
   - Use `.isVisible().catch(() => false)` pattern for safe element checks

3. **Search Functionality**
   - Search input has 500ms debounce timer
   - Must wait 600ms after typing to allow debounce + network request
   - Use `page.waitForLoadState('networkidle')` after search to ensure results loaded
   - Search filters songs by name or CCLI ID

4. **Pagination Pattern**
   - Pagination only appears if `meta.last_page > 1`
   - Prev/next buttons are disabled at boundaries
   - Page indicator shows "Seite X von Y" format
   - Clicking page button calls `goToPage()` which fetches new data

5. **Delete Confirmation Dialog**
   - Delete button click triggers confirmation modal
   - Modal shows song title: "„{title}" wird gelöscht"
   - Cancel button: data-testid="song-list-delete-cancel-button"
   - Confirm button: data-testid="song-list-delete-confirm-button"
   - Dialog uses Teleport to body (fixed positioning)
   - Transition duration: 200ms

6. **Edit Modal Integration**
   - Edit button opens SongEditModal component
   - Modal has data-testid="song-list-edit-modal"
   - Modal is shown/hidden via `showEditModal` ref
   - Modal emits 'close' and 'updated' events
   - On update, page refetches songs for current page

7. **Download Button**
   - Currently emits 'download' event (no actual download yet)
   - Button is visible but may not trigger file download
   - Tests verify button presence and no error state

8. **Translate Navigation**
   - Translate link (not button) navigates to `/songs/{id}/translate`
   - Uses `<a>` tag with href attribute
   - data-testid="song-list-translate-link"
   - Navigation is standard link behavior (no modal)

9. **German UI Text Assertions**
   - Page heading: "Song-Datenbank"
   - Description: "Verwalte alle Songs, Übersetzungen und Arrangements."
   - Empty state: "Noch keine Songs vorhanden" or "Keine Songs gefunden"
   - Delete confirmation: "Song löschen?"
   - Cancel button: "Abbrechen"
   - Delete button: "Löschen"
   - Edit button: "Bearbeiten"
   - Download button: "Herunterladen"
   - Translate button: "Übersetzen"

10. **Test Structure for Song List**
    - Test 1: Page renders with heading and description
    - Test 2: Table structure exists OR empty state is shown
    - Test 3: Song row shows structural elements (gracefully skips if no songs)
    - Test 4: Search input filters songs (gracefully skips if no songs)
    - Test 5: Pagination works (gracefully skips if not enough songs)
    - Test 6: Delete button triggers confirmation (cancel keeps song visible)
    - Test 7: Edit button opens modal
    - Test 8: Download button triggers action (no error)
    - Test 9: Translate button navigates to translate page

### Files Created
- `tests/e2e/song-db.spec.ts` — 9 E2E tests for Song Database list page

### Test Results
- 3 passed (page renders, table structure, empty state handling)
- 7 skipped (require songs in test database)
- 0 failed

### Critical Patterns
- ALWAYS use `await page.waitForLoadState('networkidle')` after navigation
- ALWAYS use data-testid selectors, never CSS selectors
- ALWAYS use German text from Vue components for assertions
- ALWAYS handle missing data gracefully with test.skip()
- ALWAYS wait for debounce timers (500ms for search)
- NEVER assert specific song names (dynamic CTS data)
- NEVER actually delete songs (cancel confirmation)

## [2026-03-02] Task 15: Song Edit Modal E2E Tests

### Key Learnings

1. **SongEditModal Component Structure**
   - Modal has data-testid="song-edit-modal" on root container
   - Close button: data-testid="song-edit-modal-close-button"
   - Title input: data-testid="song-edit-modal-title-input"
   - CCLI input: data-testid="song-edit-modal-ccli-input"
   - Copyright textarea: data-testid="song-edit-modal-copyright-textarea"
   - ArrangementConfigurator is embedded (data-testid="arrangement-configurator")

2. **Auto-Save Behavior**
   - NO explicit save button (unlike traditional forms)
   - Uses 500ms debounce on text input via `useDebounceFn`
   - Immediate save on blur (cancels pending debounce)
   - Save indicator shows "Speichert…" (saving) or "Gespeichert" (saved)
   - Saved indicator disappears after 2 seconds

3. **Modal Lifecycle**
   - Opens via edit button click on song list
   - Fetches song data on open (via `watch(show)`)
   - Shows loading spinner while fetching
   - Shows error state if fetch fails
   - Closes via X button or overlay click
   - Emits 'close' and 'updated' events

4. **Overlay Click Handling**
   - Modal uses `closeOnBackdrop` handler
   - Checks `e.target === e.currentTarget` to detect overlay clicks
   - Overlay is the fixed inset-0 div with bg-black/50
   - Clicking inside modal content does NOT close it

5. **Arrangement Configurator Integration**
   - Embedded as separate component in modal
   - Receives props: songId, arrangements, availableGroups
   - Computed from songData.arrangements and songData.groups
   - Always visible when modal is open (no conditional rendering)

6. **German UI Text Assertions**
   - Modal title: "Song bearbeiten"
   - Subtitle: "Metadaten und Arrangements verwalten"
   - Section headers: "Metadaten", "Arrangements"
   - Field labels: "Titel", "CCLI-ID", "Copyright-Text"
   - Save indicator: "Speichert…", "Gespeichert"
   - Close button: X icon (no text)

7. **Test Structure Pattern (Song Edit Modal)**
   - Test 1: Edit button opens modal
   - Test 2: Modal shows input fields (name, CCLI, copyright)
   - Test 3: Fields auto-save without explicit save button
   - Test 4: Arrangement configurator is embedded
   - Test 5: Close modal with X button
   - Test 6: Close modal with overlay click

### Files Created
- `tests/e2e/song-edit-modal.spec.ts` — 6 E2E tests for Song Edit Modal

### Test Results
- 1 passed (auth setup)
- 6 skipped (require songs in test database)
- 0 failed

### Critical Patterns
- ALWAYS use `await page.waitForLoadState('networkidle')` after navigation
- ALWAYS use data-testid selectors, never CSS selectors
- ALWAYS use German text from Vue components for assertions
- ALWAYS handle missing data gracefully with test.skip()
- ALWAYS wait for transitions with `page.waitForTimeout(300)` after modal open/close
- NEVER modify song data permanently (or restore if modified)
- NEVER test arrangement drag-and-drop (that's Task 17)

### Verification
- ✅ File created: `tests/e2e/song-edit-modal.spec.ts`
- ✅ 6 tests covering all requirements
- ✅ Tests navigate to Songs/Index first, then open modal
- ✅ Tests use data-testid selectors only
- ✅ Tests gracefully skip if no songs exist
- ✅ Tests do NOT modify song data permanently
- ✅ Tests do NOT test arrangement drag-and-drop
- ✅ LSP diagnostics: No errors
- ✅ Playwright test run: 1 passed, 6 skipped (expected)

## [2026-03-02] Task 17: Song Translation Page E2E Tests

### Key Learnings

1. **Translate Page Component Structure**
   - Translate.vue has two main sections: text loading and editor
   - URL input: data-testid="translate-url-input"
   - Fetch button: data-testid="translate-fetch-button"
   - Source textarea: data-testid="translate-source-textarea"
   - Apply button: data-testid="translate-apply-button"
   - Editor section only visible when `editorVisible` computed property is true
   - Editor visibility depends on: sourceText.trim().length > 0 OR hasExistingTranslation

2. **Two-Column Editor Layout**
   - Original column (left): data-testid="translate-original-textarea" (readonly)
   - Translation column (right): data-testid="translate-translation-textarea" (editable)
   - Both columns are rendered for each slide in each group
   - Groups are rendered with colored headers showing group name and slide count
   - Slides are ordered by group.order then slide.order

3. **Navigation to Translate Page**
   - From Song DB list: click translate link with data-testid="song-list-translate-link"
   - URL pattern: `/songs/{id}/translate`
   - Page heading: "Song uebersetzen"
   - Back button: data-testid="translate-back-button" with text "Zurueck"

4. **Text Distribution Logic**
   - Source text can be entered manually or fetched from URL
   - "Text auf Folien verteilen" button distributes text to slides
   - Text is split by newlines and distributed to slides based on line_count
   - Each slide has a line_count property that determines how many lines it should contain
   - Slides are padded with empty lines if needed

5. **Save Functionality**
   - Save button: data-testid="translate-save-button"
   - Button text: "Speichern" (or "Speichern..." when saving)
   - Clicking save calls POST `/api/songs/{id}/translation/import` with text
   - On success, redirects to `/songs?success=Uebersetzung+gespeichert`
   - On error, shows error message in red alert box

6. **Error/Info Messages**
   - Error message: red alert box with border-red-300
   - Info message: emerald alert box with border-emerald-300
   - Messages appear/disappear based on state
   - Error messages: "Bitte fuege zuerst einen Text ein.", "Text konnte nicht von der URL abgerufen werden.", "Uebersetzung konnte nicht gespeichert werden."
   - Info messages: "Text wurde auf die Folien verteilt.", "Text wurde erfolgreich abgerufen und verteilt."

7. **German UI Text Assertions**
   - Page heading: "Song uebersetzen"
   - Section header: "Uebersetzungstext laden"
   - Description: "Du kannst einen Text von einer URL abrufen oder manuell einfuegen."
   - URL label: (no explicit label, just placeholder)
   - Fetch button: "Text abrufen" or "Abrufen..." (when fetching)
   - Manual text label: "Text manuell einfuegen"
   - Apply button: "Text auf Folien verteilen"
   - Editor header: "Folien-Editor"
   - Editor description: "Links siehst du den Originaltext, rechts bearbeitest du die Uebersetzung."
   - Original label: "Original"
   - Translation label: "Uebersetzung"
   - Save button: "Speichern" or "Speichern..." (when saving)
   - Back button: "Zurueck"

8. **Test Structure for Song Translation**
   - Test 1: Navigate to translate page from song list
   - Test 2: Two-column editor layout is visible
   - Test 3: URL input field and fetch button are visible
   - Test 4: Group/slide navigation works
   - Test 5: Text editor on right column is editable
   - Test 6: Save button persists changes (verify button, don't actually save)
   - Test 7: Back button navigates to song list

### Files Created
- `tests/e2e/song-translate.spec.ts` — 7 E2E tests for Song Translation page

### Test Results
- 1 passed (auth setup)
- 7 skipped (require songs in test database)
- 0 failed

### Critical Patterns
- ALWAYS use `await page.waitForLoadState('networkidle')` after navigation
- ALWAYS use data-testid selectors, never CSS selectors
- ALWAYS use German text from Vue components for assertions
- ALWAYS handle missing data gracefully with test.skip()
- ALWAYS wait for transitions with `page.waitForTimeout(300)` after adding source text
- NEVER fetch from external URLs in tests (network dependency)
- NEVER permanently modify translation data (or restore if modified)
- Tests gracefully skip when no songs exist in database

### Verification
- ✅ File created: `tests/e2e/song-translate.spec.ts`
- ✅ 7 tests covering all requirements
- ✅ Tests navigate from Song DB to translate page
- ✅ Tests use data-testid selectors only
- ✅ Tests gracefully skip if no songs exist
- ✅ Tests do NOT fetch from external URLs
- ✅ Tests do NOT permanently modify translation data
- ✅ LSP diagnostics: No errors
- ✅ Playwright test run: 1 passed, 7 skipped (expected)

## [2026-03-02] Task 18: Song Preview Modal & PDF Download E2E Tests

### Key Learnings

1. **SongPreviewModal Component Structure**
   - Modal has data-testid="song-preview-modal" on root container
   - Close button: data-testid="song-preview-modal-close-button"
   - PDF link: data-testid="song-preview-modal-pdf-link"
   - Modal uses Teleport to body (fixed positioning)
   - Transition duration: 200ms enter, 150ms leave

2. **Modal Lifecycle**
   - Opens via preview button click on matched songs
   - Shows song title and arrangement name in header
   - Displays groups with colored labels and slides
   - Supports ESC key to close (via onMounted keydown listener)
   - Supports overlay click to close (closeOnBackdrop handler)
   - Supports X button to close (emit 'close' event)

3. **PDF Download Functionality**
   - PDF link href pattern: `/songs/{id}/arrangements/{id}/pdf`
   - Opens in new tab (target="_blank")
   - SongPdfController.download() returns PDF response with content-type: application/pdf
   - Filename generated from song title + arrangement name slugified

4. **Group Display in Modal**
   - Groups rendered in arrangement order (sorted by order field)
   - Each group has colored header with group name
   - Slides displayed under each group in order
   - Supports translation display (side-by-side columns if useTranslation=true)
   - Copyright footer shows song copyright and CCLI ID

5. **Test Pattern for Preview Modal**
   - Navigate to service edit page
   - Expand Songs block (4th accordion)
   - Find first matched song with preview button
   - Click preview button to open modal
   - Verify modal visibility and content
   - Test close mechanisms (X button, ESC key, overlay click)
   - Test PDF download link (verify href and content-type)

6. **Graceful Test Degradation**
   - Tests skip if no editable service exists
   - Tests skip if no songs exist in service
   - Tests skip if no matched songs exist (unmatched songs don't have preview button)
   - Pattern: Loop through song cards to find first with preview button visible

7. **German UI Text Assertions**
   - Modal header shows song title (dynamic)
   - Arrangement name shown in subtitle
   - PDF button text: "PDF" (icon + text)
   - Close button: X icon (no text)
   - Modal content: groups with colored labels, slides with text

8. **Test Structure for Song Preview Modal**
   - Test 1: Preview button opens SongPreviewModal
   - Test 2: Modal shows groups with labels and slides
   - Test 3: Close modal with X button
   - Test 4: Close modal with ESC key
   - Test 5: PDF download button triggers download with PDF content-type

### Files Created
- `tests/e2e/song-preview-pdf.spec.ts` — 5 E2E tests for Song Preview Modal and PDF Download

### Test Results
- 1 passed (auth setup)
- 5 skipped (require matched songs in test database)
- 0 failed

### Critical Patterns
- ALWAYS use `await page.waitForLoadState('networkidle')` after navigation
- ALWAYS use data-testid selectors, never CSS selectors
- ALWAYS handle missing data gracefully with test.skip()
- ALWAYS wait for transitions with `page.waitForTimeout(300)` after modal open/close
- NEVER assert specific song text content (dynamic data)
- NEVER validate PDF content structure (just verify content-type)
- Tests work regardless of specific song data or arrangement configuration

### Verification
- ✅ File created: `tests/e2e/song-preview-pdf.spec.ts`
- ✅ 5 tests covering all requirements
- ✅ Tests navigate to service edit → Songs block → Preview button
- ✅ Tests use data-testid selectors only
- ✅ Tests gracefully skip if no matched songs exist
- ✅ Tests do NOT assert specific song text content
- ✅ Tests do NOT validate PDF content structure (just verify content-type)
- ✅ LSP diagnostics: No errors
- ✅ Playwright test run: 1 passed, 5 skipped (expected)
