test(e2e): Playwright tests for restructured edit page

This commit is contained in:
Thorsten Bus 2026-03-29 12:26:11 +02:00
parent fb1e51361f
commit 6964931286
5 changed files with 458 additions and 684 deletions

View file

@ -0,0 +1,299 @@
import { test, expect } from '@playwright/test';
async function navigateToEditPage(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) {
return false;
}
await editButton.click();
await page.waitForLoadState('networkidle');
return true;
}
test('edit seite zeigt ablauf sektion statt accordion bloecke', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
await expect(page).toHaveURL(/.*services\/\d+\/edit/);
const agendaSection = page.getByTestId('agenda-section');
const emptyState = page.getByText('Keine Ablauf-Elemente vorhanden');
const hasAgenda = await agendaSection.isVisible().catch(() => false);
const hasEmptyState = await emptyState.isVisible().catch(() => false);
expect(hasAgenda || hasEmptyState).toBe(true);
const blockToggles = page.getByTestId('service-edit-block-toggle');
const toggleCount = await blockToggles.count();
expect(toggleCount).toBe(0);
});
test('informations block ist oben sichtbar', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const informationBlock = page.getByTestId('information-block');
await expect(informationBlock).toBeVisible();
});
test('ablauf ueberschrift ist sichtbar', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
await expect(page.getByText('Ablauf')).toBeVisible();
});
test('agenda items zeigen korrekte elemente oder empty state', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const agendaSection = page.getByTestId('agenda-section');
const emptyState = page.getByText('Keine Ablauf-Elemente vorhanden');
const hasAgenda = await agendaSection.isVisible().catch(() => false);
const hasEmptyState = await emptyState.isVisible().catch(() => false);
if (hasEmptyState) {
await expect(page.getByText('Bitte synchronisiere die Daten zuerst')).toBeVisible();
return;
}
expect(hasAgenda).toBe(true);
const agendaRows = page.getByTestId('agenda-item-row');
const songItems = page.getByTestId('song-agenda-item');
const headerItems = page.getByTestId('agenda-header-item');
const rowCount = await agendaRows.count();
const songCount = await songItems.count();
const headerCount = await headerItems.count();
expect(rowCount + songCount + headerCount).toBeGreaterThan(0);
});
test('header items zeigen titel als ueberschrift', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const headerItems = page.getByTestId('agenda-header-item');
const headerCount = await headerItems.count();
if (headerCount === 0) {
test.skip();
}
const firstHeader = headerItems.first();
await expect(firstHeader).toBeVisible();
const headerText = await firstHeader.textContent();
expect(headerText?.trim().length).toBeGreaterThan(0);
});
test('song agenda items zeigen songtitel', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const songItems = page.getByTestId('song-agenda-item');
const songCount = await songItems.count();
if (songCount === 0) {
test.skip();
}
const firstSong = songItems.first();
await expect(firstSong).toBeVisible();
const songTitle = firstSong.getByTestId('song-agenda-title');
await expect(songTitle).toBeVisible();
const titleText = await songTitle.textContent();
expect(titleText?.trim().length).toBeGreaterThan(0);
});
test('song agenda items zeigen arrangement pill wenn zugeordnet', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const arrangementPills = page.getByTestId('arrangement-pill');
const pillCount = await arrangementPills.count();
if (pillCount === 0) {
test.skip();
}
const firstPill = arrangementPills.first();
await expect(firstPill).toBeVisible();
});
test('song agenda item zeigt arrangement bearbeiten button', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const editArrangementBtn = page.getByTestId('song-edit-arrangement');
const hasBtn = await editArrangementBtn.first().isVisible().catch(() => false);
if (!hasBtn) {
test.skip();
}
await expect(editArrangementBtn.first()).toBeVisible();
});
test('generische agenda items zeigen titel', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const agendaRows = page.getByTestId('agenda-item-row');
const rowCount = await agendaRows.count();
if (rowCount === 0) {
test.skip();
}
const firstRow = agendaRows.first();
await expect(firstRow).toBeVisible();
const itemTitle = firstRow.getByTestId('agenda-item-title');
await expect(itemTitle).toBeVisible();
expect(await itemTitle.textContent()).toBeTruthy();
});
test('nicht zugeordnete songs zeigen erstellung anfragen button', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const requestBtn = page.getByTestId('song-request-creation');
const hasUnmatched = await requestBtn.first().isVisible().catch(() => false);
if (!hasUnmatched) {
test.skip();
}
await expect(requestBtn.first()).toBeVisible();
});
test('song suche und manuelle zuordnung sichtbar bei nicht zugeordnetem song', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const searchInput = page.getByTestId('song-search-input');
const hasSearch = await searchInput.first().isVisible().catch(() => false);
if (!hasSearch) {
test.skip();
}
await expect(searchInput.first()).toBeVisible();
const assignBtn = page.getByTestId('song-assign-button');
await expect(assignBtn.first()).toBeVisible();
});
test('uebersetzungs checkbox sichtbar bei songs mit uebersetzung', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const translationCheckbox = page.getByTestId('song-translation-checkbox');
const hasTranslation = await translationCheckbox.first().isVisible().catch(() => false);
if (!hasTranslation) {
test.skip();
}
const initialState = await translationCheckbox.first().isChecked();
await translationCheckbox.first().click();
await page.waitForTimeout(300);
const toggledState = await translationCheckbox.first().isChecked();
expect(toggledState).not.toBe(initialState);
await translationCheckbox.first().click();
await page.waitForTimeout(300);
const restoredState = await translationCheckbox.first().isChecked();
expect(restoredState).toBe(initialState);
});
test('sticky action bar ist sichtbar', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const actionBar = page.getByTestId('service-edit-action-bar');
await expect(actionBar).toBeVisible();
const inProgress = page.getByText('In Bearbeitung');
const finalized = page.getByText('Abgeschlossen');
const hasInProgress = await inProgress.isVisible().catch(() => false);
const hasFinalized = await finalized.isVisible().catch(() => false);
expect(hasInProgress || hasFinalized).toBe(true);
});
test('abschliessen button in action bar sichtbar', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const finalizeBtn = page.getByTestId('service-edit-finalize-button');
const hasFinalizeBtn = await finalizeBtn.isVisible().catch(() => false);
if (!hasFinalizeBtn) {
const reopenBtn = page.getByTestId('service-edit-reopen-button');
await expect(reopenBtn).toBeVisible();
return;
}
await expect(finalizeBtn).toBeVisible();
await expect(finalizeBtn).toContainText('Abschließen');
const finalizeDownloadBtn = page.getByTestId('service-edit-finalize-download-button');
await expect(finalizeDownloadBtn).toBeVisible();
});
test('zurueck button navigiert zur service liste', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const backBtn = page.getByTestId('service-edit-back-icon-button');
await expect(backBtn).toBeVisible();
await backBtn.click();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*services$/);
});

View file

@ -25,12 +25,10 @@ test('navigate to first editable service edit page', async ({ page }) => {
await expect(informationBlock).toBeVisible();
});
// Test 2: Information block accordion is visible and can be expanded/collapsed
test('information block accordion is visible and can be expanded/collapsed', async ({ page }) => {
test('information block is always visible without accordion toggle', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
// Find first unfinalized service
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
@ -41,38 +39,12 @@ test('information block accordion is visible and can be expanded/collapsed', asy
await editButton.click();
await page.waitForLoadState('networkidle');
// Find the Information block toggle button
const blockToggles = page.getByTestId('service-edit-block-toggle');
const informationToggle = blockToggles.filter({ has: page.locator('text=Information') }).first();
const toggleExists = await informationToggle.isVisible().catch(() => false);
if (!toggleExists) {
test.skip();
}
// Verify toggle button is visible
await expect(informationToggle).toBeVisible();
// Get the Information block content container
const informationBlock = page.getByTestId('information-block');
// Verify block is initially visible (expanded by default)
await expect(informationBlock).toBeVisible();
// Click toggle to collapse
await informationToggle.click();
await page.waitForTimeout(300); // Wait for transition
// Verify block is hidden
const isHidden = await informationBlock.isHidden().catch(() => true);
expect(isHidden).toBe(true);
// Click toggle again to expand
await informationToggle.click();
await page.waitForTimeout(300); // Wait for transition
// Verify block is visible again
await expect(informationBlock).toBeVisible();
const blockToggles = page.getByTestId('service-edit-block-toggle');
const toggleCount = await blockToggles.count();
expect(toggleCount).toBe(0);
});
// Test 3: Upload area is visible with drag-and-drop zone and click-to-upload

View file

@ -1,204 +1,62 @@
import { test, expect } from '@playwright/test';
// Test 1: Navigate to first editable (non-finalized) service edit page
test('navigate to first editable service edit page', async ({ page }) => {
async function navigateToEditPage(page) {
await page.goto('/services');
await page.waitForLoadState('networkidle');
// Find first unfinalized service (one with edit button)
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
return false;
}
await editButton.click();
await page.waitForLoadState('networkidle');
return true;
}
test('navigate to first editable service edit page', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
// Click edit button
await editButton.click();
await page.waitForLoadState('networkidle');
// Verify we're on the edit page
await expect(page).toHaveURL(/.*services\/\d+\/edit/);
// Verify Moderation block is visible
const moderationBlock = page.getByTestId('moderation-block');
await expect(moderationBlock).toBeVisible();
const agendaSection = page.getByTestId('agenda-section');
const emptyState = page.getByText('Keine Ablauf-Elemente vorhanden');
const hasAgenda = await agendaSection.isVisible().catch(() => false);
const hasEmptyState = await emptyState.isVisible().catch(() => false);
expect(hasAgenda || hasEmptyState).toBe(true);
});
// Test 2: Moderation block accordion is visible and can be expanded/collapsed
test('moderation block accordion is visible and can be expanded/collapsed', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
test.skip('moderation block accordion — replaced by agenda view', async () => {});
// Find first unfinalized service
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test('agenda items with slides show slide uploader', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const agendaRows = page.getByTestId('agenda-item-row');
const rowCount = await agendaRows.count();
// Find the Moderation block toggle button
const blockToggles = page.getByTestId('service-edit-block-toggle');
const moderationToggle = blockToggles.filter({ has: page.locator('text=Moderation') }).first();
const toggleExists = await moderationToggle.isVisible().catch(() => false);
if (!toggleExists) {
if (rowCount === 0) {
test.skip();
}
// Verify toggle button is visible
await expect(moderationToggle).toBeVisible();
const addSlidesBtn = page.getByTestId('agenda-item-add-slides').first();
const hasSlidesBtn = await addSlidesBtn.isVisible().catch(() => false);
// Get the Moderation block content container
const moderationBlock = page.getByTestId('moderation-block');
// Verify block is initially visible (expanded by default)
await expect(moderationBlock).toBeVisible();
if (!hasSlidesBtn) {
test.skip();
}
// Click toggle to collapse
await moderationToggle.click();
await page.waitForTimeout(300); // Wait for transition
// Verify block is hidden
const isHidden = await moderationBlock.isHidden().catch(() => true);
expect(isHidden).toBe(true);
// Click toggle again to expand
await moderationToggle.click();
await page.waitForTimeout(300); // Wait for transition
// Verify block is visible again
await expect(moderationBlock).toBeVisible();
await expect(addSlidesBtn).toBeVisible();
});
// Test 3: Upload area is visible with drag-and-drop zone (NO datepicker)
test('upload area is visible with drag-and-drop zone', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
test.skip('existing moderation slides display as thumbnails — replaced by agenda item slides', async () => {});
// Find first unfinalized service
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');
// Verify Moderation block uploader is visible
const uploader = page.getByTestId('moderation-block-uploader');
await expect(uploader).toBeVisible();
// Verify dropzone is visible
const dropzone = page.getByTestId('slide-uploader-dropzone');
await expect(dropzone).toBeVisible();
// Verify dropzone contains expected text
await expect(dropzone).toContainText('Dateien hier ablegen');
await expect(dropzone).toContainText('oder klicken zum Auswählen');
// 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 4: Existing moderation slides display as thumbnails
test('existing moderation slides display as thumbnails', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
// Find first unfinalized service
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');
// Verify slide grid is visible
const slideGrid = page.getByTestId('moderation-block-grid');
await expect(slideGrid).toBeVisible();
// Check if slides exist
const slideThumbnails = page.locator('[data-testid="slide-grid-delete-button"]');
const slideCount = await slideThumbnails.count();
if (slideCount === 0) {
// No slides exist - verify empty state message
const emptyState = slideGrid.locator('text=Noch keine Folien vorhanden');
await expect(emptyState).toBeVisible();
return;
}
// Slides exist - verify first thumbnail is visible
const firstThumbnail = page.locator('[data-testid="slide-grid-delete-button"]').first();
await expect(firstThumbnail).toBeVisible();
// Verify delete button is visible on hover
const deleteButton = firstThumbnail;
await expect(deleteButton).toBeVisible();
});
// Test 5: Delete button on moderation slide thumbnail triggers confirmation
test('delete button on moderation slide thumbnail triggers confirmation', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
// Find first unfinalized service
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 if slides exist
const slideThumbnails = page.locator('[data-testid="slide-grid-delete-button"]');
const slideCount = await slideThumbnails.count();
if (slideCount === 0) {
// No slides exist - skip this test
test.skip();
}
// Get first delete button
const firstDeleteButton = page.getByTestId('slide-grid-delete-button').first();
await expect(firstDeleteButton).toBeVisible();
// Click delete button
await firstDeleteButton.click();
await page.waitForTimeout(200);
// Verify confirmation dialog appears
const confirmDialog = page.locator('text=Folie löschen?');
await expect(confirmDialog).toBeVisible();
// Verify dialog contains expected text
await expect(page.locator('text=Möchtest du die Folie')).toBeVisible();
await expect(page.locator('text=wirklich löschen?')).toBeVisible();
// Verify cancel button is visible
const cancelButton = page.locator('button:has-text("Abbrechen")').first();
await expect(cancelButton).toBeVisible();
// Click cancel to close dialog without deleting
await cancelButton.click();
await page.waitForTimeout(200);
// Verify dialog is closed
const dialogClosed = await confirmDialog.isHidden().catch(() => true);
expect(dialogClosed).toBe(true);
});
test.skip('delete button on moderation slide triggers confirmation — replaced by agenda item slides', async () => {});

View file

@ -1,204 +1,41 @@
import { test, expect } from '@playwright/test';
// Test 1: Navigate to first editable (non-finalized) service edit page
test('navigate to first editable service edit page', async ({ page }) => {
async function navigateToEditPage(page) {
await page.goto('/services');
await page.waitForLoadState('networkidle');
// Find first unfinalized service (one with edit button)
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
return false;
}
await editButton.click();
await page.waitForLoadState('networkidle');
return true;
}
test('navigate to first editable service edit page', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
// Click edit button
await editButton.click();
await page.waitForLoadState('networkidle');
// Verify we're on the edit page
await expect(page).toHaveURL(/.*services\/\d+\/edit/);
// Verify Sermon block is visible
const sermonBlock = page.getByTestId('sermon-block');
await expect(sermonBlock).toBeVisible();
const agendaSection = page.getByTestId('agenda-section');
const emptyState = page.getByText('Keine Ablauf-Elemente vorhanden');
const hasAgenda = await agendaSection.isVisible().catch(() => false);
const hasEmptyState = await emptyState.isVisible().catch(() => false);
expect(hasAgenda || hasEmptyState).toBe(true);
});
// Test 2: Sermon block accordion is visible and can be expanded/collapsed
test('sermon block accordion is visible and can be expanded/collapsed', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
test.skip('sermon block accordion — replaced by agenda view', async () => {});
// Find first unfinalized service
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
test.skip('sermon upload area — replaced by agenda item slide uploader', async () => {});
if (!hasEditableService) {
test.skip();
}
test.skip('existing sermon slides as thumbnails — replaced by agenda item slides', async () => {});
await editButton.click();
await page.waitForLoadState('networkidle');
// Find the Sermon block toggle button (3rd block)
const blockToggles = page.getByTestId('service-edit-block-toggle');
const sermonToggle = blockToggles.filter({ has: page.locator('text=Predigt') }).first();
const toggleExists = await sermonToggle.isVisible().catch(() => false);
if (!toggleExists) {
test.skip();
}
// Verify toggle button is visible
await expect(sermonToggle).toBeVisible();
// Get the Sermon block content container
const sermonBlock = page.getByTestId('sermon-block');
// Verify block is initially visible (expanded by default)
await expect(sermonBlock).toBeVisible();
// Click toggle to collapse
await sermonToggle.click();
await page.waitForTimeout(300); // Wait for transition
// Verify block is hidden
const isHidden = await sermonBlock.isHidden().catch(() => true);
expect(isHidden).toBe(true);
// Click toggle again to expand
await sermonToggle.click();
await page.waitForTimeout(300); // Wait for transition
// Verify block is visible again
await expect(sermonBlock).toBeVisible();
});
// Test 3: Upload area is visible with drag-and-drop zone (NO datepicker)
test('upload area is visible with drag-and-drop zone', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
// Find first unfinalized service
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');
// Verify Sermon block uploader is visible
const uploader = page.getByTestId('sermon-block-uploader');
await expect(uploader).toBeVisible();
// Verify dropzone is visible
const dropzone = page.getByTestId('slide-uploader-dropzone');
await expect(dropzone).toBeVisible();
// Verify dropzone contains expected text
await expect(dropzone).toContainText('Dateien hier ablegen');
await expect(dropzone).toContainText('oder klicken zum Auswählen');
// 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 4: Existing sermon slides display as thumbnails
test('existing sermon slides display as thumbnails', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
// Find first unfinalized service
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');
// Verify slide grid is visible
const slideGrid = page.getByTestId('sermon-block-grid');
await expect(slideGrid).toBeVisible();
// Check if slides exist
const slideThumbnails = page.locator('[data-testid="slide-grid-delete-button"]');
const slideCount = await slideThumbnails.count();
if (slideCount === 0) {
// No slides exist - verify empty state message
const emptyState = slideGrid.locator('text=Noch keine Folien vorhanden');
await expect(emptyState).toBeVisible();
return;
}
// Slides exist - verify first thumbnail is visible
const firstThumbnail = page.locator('[data-testid="slide-grid-delete-button"]').first();
await expect(firstThumbnail).toBeVisible();
// Verify delete button is visible on hover
const deleteButton = firstThumbnail;
await expect(deleteButton).toBeVisible();
});
// Test 5: Delete button on sermon slide thumbnail triggers confirmation
test('delete button on sermon slide thumbnail triggers confirmation', async ({ page }) => {
await page.goto('/services');
await page.waitForLoadState('networkidle');
// Find first unfinalized service
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 if slides exist
const slideThumbnails = page.locator('[data-testid="slide-grid-delete-button"]');
const slideCount = await slideThumbnails.count();
if (slideCount === 0) {
// No slides exist - skip this test
test.skip();
}
// Get first delete button
const firstDeleteButton = page.getByTestId('slide-grid-delete-button').first();
await expect(firstDeleteButton).toBeVisible();
// Click delete button
await firstDeleteButton.click();
await page.waitForTimeout(200);
// Verify confirmation dialog appears
const confirmDialog = page.locator('text=Folie löschen?');
await expect(confirmDialog).toBeVisible();
// Verify dialog contains expected text
await expect(page.locator('text=Möchtest du die Folie')).toBeVisible();
await expect(page.locator('text=wirklich löschen?')).toBeVisible();
// Verify cancel button is visible
const cancelButton = page.locator('button:has-text("Abbrechen")').first();
await expect(cancelButton).toBeVisible();
// Click cancel to close dialog without deleting
await cancelButton.click();
await page.waitForTimeout(200);
// Verify dialog is closed
const dialogClosed = await confirmDialog.isHidden().catch(() => true);
expect(dialogClosed).toBe(true);
});
test.skip('delete button on sermon slide — replaced by agenda item slides', async () => {});

View file

@ -1,7 +1,6 @@
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 }) => {
async function navigateToEditPage(page) {
await page.goto('/services');
await page.waitForLoadState('networkidle');
@ -9,375 +8,184 @@ test('songs block accordion can be expanded and collapsed', async ({ page }) =>
const hasEditableService = await editButton.isVisible().catch(() => false);
if (!hasEditableService) {
test.skip();
return false;
}
await editButton.click();
await page.waitForLoadState('networkidle');
return true;
}
// 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();
test.skip('songs block accordion — replaced by agenda view', async () => {});
const toggleExists = await songsToggle.isVisible().catch(() => false);
if (!toggleExists) {
test('song items visible in agenda or empty state', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
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();
const songItems = page.getByTestId('song-agenda-item');
const songCount = await songItems.count();
if (songCount === 0) {
// No songs - verify empty state message
await expect(songsBlock).toContainText('Fuer diesen Service sind aktuell keine Songs vorhanden.');
const emptyState = page.getByText('Keine Ablauf-Elemente vorhanden');
const hasEmptyState = await emptyState.isVisible().catch(() => false);
expect(hasEmptyState).toBe(true);
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();
const firstSongItem = songItems.first();
await expect(firstSongItem).toBeVisible();
const songTitle = firstSongItem.getByTestId('song-agenda-title');
await expect(songTitle).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('song agenda item shows title and ccli info', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const songCards = page.getByTestId('songs-block-song-card');
const songCount = await songCards.count();
const songItems = page.getByTestId('song-agenda-item');
const songCount = await songItems.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();
const firstSongItem = songItems.first();
const songTitle = firstSongItem.getByTestId('song-agenda-title');
await expect(songTitle).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('unmatched songs show request creation button in agenda', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
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);
const requestButton = page.getByTestId('song-request-creation').first();
const hasUnmatched = await requestButton.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();
const searchInput = page.getByTestId('song-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();
const assignButton = page.getByTestId('song-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('matched songs show arrangement pill in agenda', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const arrangementPill = page.getByTestId('arrangement-pill').first();
const hasPill = await arrangementPill.isVisible().catch(() => false);
// 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) {
if (!hasPill) {
test.skip();
}
await expect(arrangementPill).toBeVisible();
});
test('arrangement edit button opens arrangement dialog', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const editArrangementBtn = page.getByTestId('song-edit-arrangement').first();
const hasBtn = await editArrangementBtn.isVisible().catch(() => false);
if (!hasBtn) {
test.skip();
}
await editArrangementBtn.click();
await page.waitForTimeout(500);
const arrangementDialog = page.getByTestId('arrangement-dialog');
await expect(arrangementDialog).toBeVisible();
const closeBtn = page.getByTestId('arrangement-dialog-close-btn');
await closeBtn.click();
await page.waitForTimeout(300);
});
test('arrangement dialog has select, add, clone buttons', async ({ page }) => {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
const editArrangementBtn = page.getByTestId('song-edit-arrangement').first();
const hasBtn = await editArrangementBtn.isVisible().catch(() => false);
if (!hasBtn) {
test.skip();
}
await editArrangementBtn.click();
await page.waitForTimeout(500);
const arrangementDialog = page.getByTestId('arrangement-dialog');
const dialogVisible = await arrangementDialog.isVisible().catch(() => false);
if (!dialogVisible) {
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();
const addButton = page.getByTestId('arrangement-new-btn').first();
await expect(addButton).toBeVisible();
await expect(addButton).toContainText('Hinzufügen');
// Verify clone button
const cloneButton = page.getByTestId('arrangement-clone-button').first();
const cloneButton = page.getByTestId('arrangement-clone-btn').first();
await expect(cloneButton).toBeVisible();
await expect(cloneButton).toContainText('Klonen');
const closeBtn = page.getByTestId('arrangement-dialog-close-btn');
await closeBtn.click();
await page.waitForTimeout(300);
});
// 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');
test.skip('preview button is present for matched songs — replaced by agenda view', async () => {});
const editButton = page.getByTestId('service-list-edit-button').first();
const hasEditableService = await editButton.isVisible().catch(() => false);
test.skip('download button is present for matched songs — replaced by agenda view', async () => {});
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) {
const navigated = await navigateToEditPage(page);
if (!navigated) {
test.skip();
}
await editButton.click();
await page.waitForLoadState('networkidle');
const translationCheckbox = page.getByTestId('songs-block-translation-checkbox').first();
const translationCheckbox = page.getByTestId('song-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);
});