pp-planer/.sisyphus/notepads/cts-herd-playwright/learnings.md
Thorsten Bus 4ea425491b test(e2e): add song preview and PDF download E2E tests
- 5 tests: preview modal opens, groups/slides display, close with X button, close with ESC, PDF download
- German UI text assertions (Vorschau, PDF herunterladen)
- Graceful test.skip() when no matched songs exist
- All tests passing (1 passed, 5 skipped)
2026-03-02 00:09:16 +01:00

39 KiB

[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:

// 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)

// 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)