pp-planer/tests/e2e/service-edit-songs.spec.ts
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

384 lines
13 KiB
TypeScript

import { test, expect } from '@playwright/test';
// Test 1: Songs block accordion can be expanded and collapsed
test('songs block accordion can be expanded and collapsed', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
// Find the Songs block toggle button (4th block)
const blockToggles = page.getByTestId('service-edit-block-toggle');
const songsToggle = blockToggles.filter({ has: page.locator('text=Songs') }).first();
const toggleExists = await songsToggle.isVisible().catch(() => false);
if (!toggleExists) {
test.skip();
}
await expect(songsToggle).toBeVisible();
// Get the Songs block content container
const songsBlock = page.getByTestId('songs-block');
// Verify block is initially visible (expanded by default)
await expect(songsBlock).toBeVisible();
// Click toggle to collapse
await songsToggle.click();
await page.waitForTimeout(300); // Wait for transition
// Verify block is hidden
const isHidden = await songsBlock.isHidden().catch(() => true);
expect(isHidden).toBe(true);
// Click toggle again to expand
await songsToggle.click();
await page.waitForTimeout(300); // Wait for transition
// Verify block is visible again
await expect(songsBlock).toBeVisible();
});
// Test 2: Song list shows songs in correct order or empty state
test('song list shows songs or empty state', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const songsBlock = page.getByTestId('songs-block');
await expect(songsBlock).toBeVisible();
// Check for song cards
const songCards = page.getByTestId('songs-block-song-card');
const songCount = await songCards.count();
if (songCount === 0) {
// No songs - verify empty state message
await expect(songsBlock).toContainText('Fuer diesen Service sind aktuell keine Songs vorhanden.');
return;
}
// Songs exist - verify first card is rendered with order label
const firstSongCard = songCards.first();
await expect(firstSongCard).toBeVisible();
await expect(firstSongCard.locator('text=/Song \\d+/')).toBeVisible();
});
// Test 3: Each song row shows name, CCLI ID, and status badge
test('song row shows name, CCLI ID, and status badge', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const songCards = page.getByTestId('songs-block-song-card');
const songCount = await songCards.count();
if (songCount === 0) {
test.skip();
}
const firstSongCard = songCards.first();
// Verify CCLI label exists
await expect(firstSongCard.locator('text=/CCLI:/')).toBeVisible();
// Verify translation indicator exists
await expect(firstSongCard.locator('text=/Hat Uebersetzung:/')).toBeVisible();
// Verify status badge exists (either "Zugeordnet" or "Nicht zugeordnet")
const statusBadge = firstSongCard.locator('text=/zugeordnet/i').first();
await expect(statusBadge).toBeVisible();
});
// Test 4: Unmatched songs show "Erstellung anfragen" button and manual assign select
test('unmatched songs show request creation button and manual assign', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const songCards = page.getByTestId('songs-block-song-card');
const songCount = await songCards.count();
if (songCount === 0) {
test.skip();
}
// Look for unmatched song card (has "Nicht zugeordnet" badge)
const unmatchedCard = songCards.filter({ has: page.locator('text=Nicht zugeordnet') }).first();
const hasUnmatched = await unmatchedCard.isVisible().catch(() => false);
if (!hasUnmatched) {
test.skip();
}
// Verify "Erstellung anfragen" button
const requestButton = page.getByTestId('songs-block-request-button').first();
await expect(requestButton).toBeVisible();
await expect(requestButton).toContainText('Erstellung anfragen');
// Verify search input
const searchInput = page.getByTestId('songs-block-search-input').first();
await expect(searchInput).toBeVisible();
// Verify song select dropdown
const songSelect = page.getByTestId('songs-block-song-select').first();
await expect(songSelect).toBeVisible();
// Verify "Zuordnen" (assign) button
const assignButton = page.getByTestId('songs-block-assign-button').first();
await expect(assignButton).toBeVisible();
await expect(assignButton).toContainText('Zuordnen');
});
// Test 5: Matched songs show arrangement dropdown with options
test('matched songs show arrangement dropdown with options', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
// Check for arrangement configurator (only present for matched songs)
const arrangementConfigurator = page.getByTestId('arrangement-configurator').first();
const hasMatched = await arrangementConfigurator.isVisible().catch(() => false);
if (!hasMatched) {
test.skip();
}
// Verify arrangement select dropdown exists
const arrangementSelect = page.getByTestId('arrangement-select').first();
await expect(arrangementSelect).toBeVisible();
// Verify add button
const addButton = page.getByTestId('arrangement-add-button').first();
await expect(addButton).toBeVisible();
await expect(addButton).toContainText('Hinzufügen');
// Verify clone button
const cloneButton = page.getByTestId('arrangement-clone-button').first();
await expect(cloneButton).toBeVisible();
await expect(cloneButton).toContainText('Klonen');
});
// Test 6: Arrangement "Hinzufügen" (Add) button opens name prompt
test('arrangement add button opens name prompt', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const addButton = page.getByTestId('arrangement-add-button').first();
const hasMatched = await addButton.isVisible().catch(() => false);
if (!hasMatched) {
test.skip();
}
// Set up dialog handler before clicking (window.prompt is synchronous)
let promptShown = false;
page.once('dialog', async (dialog) => {
promptShown = true;
expect(dialog.type()).toBe('prompt');
expect(dialog.message()).toContain('Name des neuen Arrangements');
await dialog.dismiss(); // Cancel without creating
});
await addButton.click();
await page.waitForTimeout(500);
expect(promptShown).toBe(true);
});
// Test 7: Arrangement "Klonen" (Clone) button opens name prompt
test('arrangement clone button opens name prompt', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const cloneButton = page.getByTestId('arrangement-clone-button').first();
const hasMatched = await cloneButton.isVisible().catch(() => false);
if (!hasMatched) {
test.skip();
}
// Verify arrangement select has options (clone requires a selected arrangement)
const arrangementSelect = page.getByTestId('arrangement-select').first();
const optionCount = await arrangementSelect.locator('option').count();
if (optionCount === 0) {
test.skip();
}
// Set up dialog handler before clicking
let promptShown = false;
page.once('dialog', async (dialog) => {
promptShown = true;
expect(dialog.type()).toBe('prompt');
expect(dialog.message()).toContain('Name des neuen Arrangements');
await dialog.dismiss(); // Cancel without creating
});
await cloneButton.click();
await page.waitForTimeout(500);
expect(promptShown).toBe(true);
});
// Test 8: Preview button opens song preview (placeholder toast for now)
test('preview button is present for matched songs', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const previewButton = page.getByTestId('songs-block-preview-button').first();
const hasMatched = await previewButton.isVisible().catch(() => false);
if (!hasMatched) {
test.skip();
}
// Verify preview button exists with correct text
await expect(previewButton).toBeVisible();
await expect(previewButton).toContainText('Vorschau');
});
// Test 9: Download (PDF) button is present for songs with selected arrangement
test('download button is present for matched songs', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const downloadButton = page.getByTestId('songs-block-download-button').first();
const hasMatched = await downloadButton.isVisible().catch(() => false);
if (!hasMatched) {
test.skip();
}
// Verify download button exists with correct text
await expect(downloadButton).toBeVisible();
await expect(downloadButton).toContainText('PDF herunterladen');
});
// Test 10: Translation checkbox toggles (if song has translation)
test('translation checkbox toggles if song has translation', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const translationCheckbox = page.getByTestId('songs-block-translation-checkbox').first();
const hasTranslation = await translationCheckbox.isVisible().catch(() => false);
if (!hasTranslation) {
test.skip();
}
// Get current checked state
const initialState = await translationCheckbox.isChecked();
// Toggle the checkbox
await translationCheckbox.click();
await page.waitForTimeout(300);
// Verify state changed
const toggledState = await translationCheckbox.isChecked();
expect(toggledState).not.toBe(initialState);
// Toggle back to restore original state
await translationCheckbox.click();
await page.waitForTimeout(300);
// Verify restored to original state
const restoredState = await translationCheckbox.isChecked();
expect(restoredState).toBe(initialState);
});