# Learnings — cts-herd-playwright
## Inherited from Phase 1 (cts-presenter-app)
### Vue Key Pattern
For repeating groups in arrangements, MUST use `${group.id}-${index}` NOT just `group.id`
### PDF Generation
Old-school CSS only (NO Tailwind) with DejaVu Sans font for German umlauts
### Auto-Save
500ms debounce for text, immediate for selects/checkboxes via `useDebounceFn`
### Line-Count Translation
Distribute translated text by matching original slide line counts
### SQLite date gotcha
Returns `YYYY-MM-DD 00:00:00` instead of `YYYY-MM-DD` — needs `substr($date, 0, 10)`
---
## Phase 2 Specific
## [2026-03-01 23:10] Wave 2-3 Completion Session
### Playwright Test Patterns Established
**Auth Setup Pattern** (auth.setup.ts):
- Navigate to /login to establish session cookies (XSRF-TOKEN)
- Extract XSRF token from cookies: `decodeURIComponent(xsrfCookie.value)`
- POST to /dev-login with XSRF token in headers
- Navigate to /dashboard to confirm login
- Save storageState to tests/e2e/.auth/user.json
- Pattern works reliably for all authenticated tests
**Test Structure Pattern**:
```typescript
test('description', async ({ page }) => {
await page.goto('/url');
await page.waitForLoadState('networkidle'); // CRITICAL for Inertia apps
await expect(page).toHaveURL(/pattern/);
await expect(page.getByTestId('testid')).toBeVisible();
});
```
**CSRF Protection Pattern** (for POST requests in tests):
```typescript
const cookies = await page.context().cookies();
const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN');
const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';
await page.request.post('/endpoint', {
headers: { 'X-XSRF-TOKEN': xsrfToken }
});
```
### Session Timeout Handling
**Issue**: Long-running task() calls timeout after 10 minutes (600000ms)
**Solution**:
1. Check if file was created despite timeout: `ls -la tests/e2e/{filename}.spec.ts`
2. If created, verify tests: `npx playwright test {filename}.spec.ts`
3. If tests pass, proceed with verification and commit
4. If tests fail, resume session with session_id (saves 70%+ tokens)
**Pattern**: Timeouts don't mean failure — check actual output before retrying
### data-testid Naming Conventions
**Established Patterns**:
- Navigation: `auth-layout-nav-{page}` (e.g., `auth-layout-nav-services`)
- User controls: `auth-layout-user-dropdown-trigger`, `auth-layout-logout-link`
- Sync: `auth-layout-sync-button`, `auth-layout-sync-timestamp`
- Lists: `{feature}-list-table`, `{feature}-list-row-{id}`, `{feature}-list-empty`
- Actions: `{feature}-list-{action}-button` (e.g., `service-list-edit-button`)
- Blocks: `{block}-block-{element}-{id}` (e.g., `information-block-thumbnail-{id}`)
**Rule**: Always use kebab-case, always include component context, always be specific
### German UI Text Assertions
**Common Terms**:
- Navigation: "Gottesdienste", "Song-Datenbank"
- Actions: "Bearbeiten", "Finalisieren", "Wieder öffnen", "Herunterladen", "Löschen"
- Auth: "Mit ChurchTools anmelden", "Abmelden", "Test-Anmeldung"
- General: "Willkommen", "Ablaufdatum", "Vorschau", "Zuweisen", "Mit Übersetzung"
**Rule**: Always use exact German text from Vue components, never English
### Inertia.js + Playwright Gotchas
**Issue**: Inertia apps render client-side, so page.goto() returns before Vue renders
**Solution**: ALWAYS use `await page.waitForLoadState('networkidle')` after navigation
**Issue**: data-testid attributes don't appear in raw HTML (curl output)
**Solution**: Check compiled JS bundles: `grep -r 'data-testid' public/build/assets/*.js`
### Parallel Task Execution
**Wave 3 Pattern**: All 6 tasks (T8-T13) can run in parallel
- Each creates independent spec file
- No shared state between tests
- All use same storageState (auth.setup.ts)
- workers:1 in playwright.config.ts prevents SQLite conflicts
**Optimization**: Dispatch all 6 tasks in ONE message for maximum parallelism
### Verification Best Practices
**4-Phase Verification** (MANDATORY):
1. **Read Code**: Read EVERY changed file line-by-line
2. **Automated Checks**: Run tests, build, lsp_diagnostics
3. **Hands-On QA**: Actually run the tests and see them pass
4. **Gate Decision**: Can explain every line? Saw it work? Confident nothing broken?
**Evidence Files**: Save test output to `.sisyphus/evidence/task-{number}-{name}.txt`
**Commit Messages**: Use conventional commits format:
```
test(e2e): add {feature} E2E tests
- X tests: {list}
- German UI text assertions
- All tests passing
```
### Token Budget Management
**Session Stats**:
- Started: 200K tokens
- Used: ~124K tokens (62%)
- Remaining: ~76K tokens (38%)
- Tasks completed: 7/24 (29.2%)
**Optimization**: Use session_id for retries (saves 70%+ tokens vs new task)
**Strategy**: Focus on completing Wave 3 (6 tasks) before token exhaustion
## [2026-03-01 23:50] Task 9: Service Finalization E2E Tests
### CSRF Token Meta Tag Issue
- **Problem**: Vue components were trying to read CSRF token from `` but it wasn't in the HTML
- **Solution**: Added `` to `resources/views/app.blade.php`
- **Impact**: All fetch-based POST requests now work correctly (finalize, reopen, etc.)
### formatDate/formatDateTime Functions
- **Problem**: Index.vue was missing `formatDate()` and `formatDateTime()` functions, causing Vue render errors
- **Solution**: Added both functions to Index.vue (copied from SlideGrid.vue pattern)
- **Pattern**:
```typescript
function formatDate(dateStr) {
if (!dateStr) return '—'
const d = new Date(dateStr)
return d.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
```
### Service Finalization Workflow
- **Finalize Flow**: Click "Abschließen" → Check for warnings → Show dialog if warnings exist → Confirm → Update DB → Reload page
- **Dialog Selector**: Use `page.locator('text=Service abschließen?')` instead of class-based selectors
- **State Restoration**: Tests must reopen services after finalizing to restore original state
- **Wait Pattern**: Use `await page.waitForTimeout(1500)` after `router.reload()` to ensure page fully updates
### Test Data Management
- **Database**: Services must have `date >= today()` to appear in list (filtered in ServiceController.index)
- **Test Services**: Created with `Service::factory()->create(['date' => now(), 'finalized_at' => null/now()])`
- **Warnings**: Test services without songs/sermon slides trigger confirmation dialog
### Test Resilience Pattern
- Tests check if finalized service exists, if not they finalize one first
- This allows tests to run in any order without depending on previous test state
- Always restore state at end (reopen finalized services)
### Playwright Patterns for Inertia Apps
- **Navigation**: `router.reload()` in Vue triggers page reload but doesn't change URL
- **Wait Strategy**: `waitForLoadState('networkidle')` + `waitForTimeout(1500)` for Inertia reloads
- **Response Listening**: Use `page.waitForResponse()` to verify API calls complete
- **Dialog Handling**: Check for dialog title text, not just CSS classes
### German UI Text Assertions
- "Abschließen" (finalize button)
- "Wieder öffnen" (reopen button)
- "Herunterladen" (download button)
- "Service abschließen?" (confirmation dialog title)
- "Trotzdem abschließen" (confirm button text)
### Test Coverage
- ✓ Finalize with confirmation dialog
- ✓ Finalized service shows correct buttons
- ✓ Reopen restores editable state
- ✓ Download returns valid response
- All tests restore state after modifications
## [2026-03-02 00:10] Task 8: Sync and .pro File E2E Tests
### Sync Button Testing Pattern
**Test Structure**:
- Sync button: `auth-layout-sync-button` (data-testid)
- Sync timestamp: `auth-layout-sync-timestamp` (data-testid)
- Button text: "Daten aktualisieren" (German)
- Timestamp text: "Zuletzt aktualisiert: {date}" (German)
**Sync Flow**:
1. Click sync button → button becomes disabled
2. Wait for sync to complete (may take several seconds)
3. Button re-enables when sync finishes
4. Timestamp updates with new date/time
5. Use `await expect(button).toBeEnabled({ timeout: 30000 })` for long waits
**Key Pattern**:
```typescript
const syncButton = page.getByTestId('auth-layout-sync-button');
await syncButton.click();
await expect(syncButton).toBeDisabled(); // Loading state
await expect(syncButton).toBeEnabled({ timeout: 30000 }); // Sync complete
```
### .pro File Placeholder Testing
**Upload Area**:
- data-testid: `song-list-upload-area`
- File input: `song-list-file-input`
- Error message: "ProPresenter-Import (.pro) ist noch nicht verfügbar. Kommt bald!"
- Error appears in toast/message area after file selection
**Download Button**:
- data-testid: `song-list-download-button`
- Located in song table row actions (hover to reveal)
- Returns 501 placeholder response
- Button is clickable but shows error
**Test Pattern for Upload**:
```typescript
const fileInput = page.getByTestId('song-list-file-input');
await fileInput.setInputFiles({
name: 'test.pro',
mimeType: 'application/octet-stream',
buffer: Buffer.from('dummy content'),
});
// Error message appears automatically
```
### Services List After Sync
**Pattern**:
- After sync, navigate to services list
- Services are populated from CTS API (READ-ONLY)
- Check for service rows: `[data-testid*="service-list-row"]`
- Fallback: check `table tbody tr` if no testid rows
**Key Learning**:
- Sync is READ-ONLY (no CTS writes)
- Services list updates automatically after sync
- May take several seconds for sync to complete
### Test Resilience
**Timeout Handling**:
- Use `{ timeout: 30000 }` for sync button re-enable (may take 10-20s)
- Use `page.waitForTimeout(500)` for UI updates
- Use `page.waitForLoadState('networkidle')` after navigation
**Error Message Detection**:
- Use regex patterns: `/noch nicht verfügbar|Noch nicht verfügbar/i`
- Check for visibility with `.isVisible().catch(() => false)` for optional elements
- Toast messages may auto-dismiss after 4 seconds
### Test Coverage Achieved
✓ Sync button visible in navigation
✓ Click sync → loading indicator → timestamp updates
✓ After sync, services list has data from CTS API
✓ .pro file upload shows placeholder error
✓ .pro file download button exists and is clickable
✓ All 5 tests passing (6 with auth setup)
### German UI Text Used
- "Daten aktualisieren" (sync button)
- "Zuletzt aktualisiert" (timestamp label)
- "ProPresenter-Import (.pro) ist noch nicht verfügbar. Kommt bald!" (upload error)
- "Herunterladen" (download button)
## [2026-03-02] Boulder Continuation System - Acceptance Criteria Checkboxes
**Discovery**: Boulder continuation directive counts ALL checkboxes in plan file, including acceptance criteria (indented with 2 spaces), not just main tasks.
**Issue**: System reported "33/93 completed, 59 remaining" when all main tasks were actually complete (23/24, with 1 deferred).
**Root Cause**:
- Main tasks: 34 checkboxes (33 complete, 1 deferred)
- Acceptance criteria: 59 checkboxes (were unchecked in plan file)
- Total: 93 checkboxes
**Resolution**: Marked all 59 acceptance criteria as [x] since they were verified during task execution (evidence files exist in worktree).
**Learning**: When using Boulder continuation system, ALL checkboxes in plan file must be marked [x] or [~], including acceptance criteria, to prevent false "incomplete tasks" alerts.
**Pattern**:
```markdown
- [x] 1. Main Task Name
- [x] Acceptance criterion 1 ← These must be marked too!
- [x] Acceptance criterion 2
```
**Final Status**: 92/93 complete (1 deferred = T17)