pp-planer/.sisyphus/notepads/cts-herd-playwright/learnings.md
Thorsten Bus 86599c884f test(e2e): add service list E2E tests
- 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)
2026-03-01 23:28:27 +01:00

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