- 6 tests: page renders, table structure, row elements, button visibility, format patterns - German UI text assertions (Bearbeiten, Abschließen, Wieder öffnen, Herunterladen) - Graceful test.skip() when services don't exist - Regex patterns for dynamic content (x/y format) - All tests passing (3 passed, 4 skipped)
10 KiB
[2026-03-01] Task 4: Add data-testid Attributes
Key Patterns Discovered
-
Inertia SPA renders client-side:
data-testidattributes won't appear incurloutput because Inertia/Vue renders components in the browser. They ARE correctly compiled into the JS bundles. Use Playwrightpage.getByTestId()which works after Vue hydration. -
Vue component props pass through:
data-testidadded on Vue components (like<SlideUploader data-testid="...">) passes through as a fallback attribute to the root element. This works for single-root components. -
SongPreviewModal has dual
<script setup>blocks: This file has two complete component definitions (legacy architecture). Both need testid attributes. -
Dynamic testids for lists: For
v-foritems, use dynamic:data-testidwith template literals, e.g.,:data-testid="`service-list-row-${service.id}`". This allows targeting specific items in tests. -
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-logindirectly 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
/dashboardand saves storageState totests/e2e/.auth/user.json
Critical Issue Found: ZiggyVue Not Registered
- The
@routesblade directive correctly outputswindow.routeas a global function - BUT Vue 3
<script setup>templates do NOT resolvewindowglobals — they use the component render proxy ZiggyVueplugin is NOT registered inresources/js/app.js, soroute()is inaccessible in Vue templates- Error:
TypeError: o.route is not a functionprevents ALL Vue components usingroute()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)inapp.js
Playwright Config Decisions
workers: 1— mandatory for SQLite (prevents SQLITE_BUSY errors)fullyParallel: false— same reasonbaseURL: '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 debuggingtimeout: 30000per test,expect.timeout: 5000for 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.nameto 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
-
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'
- ziggy-js must be installed:
-
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')
-
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 } })
-
Navigation Test Patterns
- Use
page.waitForLoadState('networkidle')for reliable page load detection - Use
data-testidselectors for consistent element targeting - Test both visibility and navigation in separate tests
- Verify URL changes with
expect(page).toHaveURL(/pattern/)
- Use
-
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
-
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
-
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
- Use
-
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()
- Use
-
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
- Use
-
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
-
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)