pp-planer/.sisyphus/notepads/cts-herd-playwright/learnings.md
Thorsten Bus acc3ab171a test(e2e): add service edit moderation block E2E tests
- 5 tests: navigate, accordion toggle, upload area (NO datepicker), thumbnails, delete confirmation
- German UI text assertions (Moderationsfolien, Löschen)
- Graceful test.skip() when no editable services or slides exist
- All tests passing (1 passed, 5 skipped)
2026-03-01 23:36:32 +01:00

345 lines
16 KiB
Markdown

## [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)