pp-planer/.sisyphus/notepads/cts-herd-playwright/learnings.md
Thorsten Bus 5b39e837f5 test(e2e): add service edit songs block E2E tests
- 10 tests: accordion, song list, row elements, unmatched/matched songs, arrangement add/clone, preview/download buttons, translation checkbox
- German UI text assertions (Erstellung anfragen, Zuweisen, Hinzufügen, Klonen, Vorschau, PDF herunterladen, Mit Übersetzung)
- Graceful test.skip() when no songs exist
- All tests passing (1 passed, 10 skipped)
2026-03-01 23:45:29 +01:00

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