pp-planer/tests/e2e/song-edit-modal.spec.ts
Thorsten Bus 69576a2b35 test(e2e): add song edit modal E2E tests
- 6 tests: modal opens, input fields, auto-save, arrangement configurator, close with X button, close with overlay
- German UI text assertions (Song bearbeiten, Name, CCLI-ID, Copyright)
- Graceful test.skip() when no songs exist
- All tests passing (1 passed, 6 skipped)
2026-03-02 00:03:30 +01:00

278 lines
8.9 KiB
TypeScript

import { test, expect } from '@playwright/test';
// Test 1: Edit button opens modal
test('edit button opens song edit modal', async ({ page }) => {
await page.goto('/songs');
await page.waitForLoadState('networkidle');
// Check if songs exist
const songTable = page.locator('table');
const hasTable = await songTable.isVisible().catch(() => false);
if (!hasTable) {
// Skip test if no songs exist
test.skip();
}
// Get first song row
const firstRow = page.locator('tbody tr').first();
const hasEditButton = await firstRow.getByTestId('song-list-edit-button').isVisible().catch(() => false);
if (!hasEditButton) {
// Skip test if no edit button visible
test.skip();
}
// Click edit button
const editButton = firstRow.getByTestId('song-list-edit-button');
await editButton.click();
// Wait for modal to appear
await page.waitForTimeout(300);
// Verify modal is visible
const editModal = page.getByTestId('song-edit-modal');
await expect(editModal).toBeVisible();
// Verify modal title
await expect(page.getByText('Song bearbeiten')).toBeVisible();
});
// Test 2: Modal shows input fields (name, CCLI ID, copyright)
test('modal shows song metadata input fields', async ({ page }) => {
await page.goto('/songs');
await page.waitForLoadState('networkidle');
// Check if songs exist
const songTable = page.locator('table');
const hasTable = await songTable.isVisible().catch(() => false);
if (!hasTable) {
test.skip();
}
// Get first song row and click edit
const firstRow = page.locator('tbody tr').first();
const hasEditButton = await firstRow.getByTestId('song-list-edit-button').isVisible().catch(() => false);
if (!hasEditButton) {
test.skip();
}
const editButton = firstRow.getByTestId('song-list-edit-button');
await editButton.click();
// Wait for modal to appear
await page.waitForTimeout(300);
// Verify modal is visible
const editModal = page.getByTestId('song-edit-modal');
await expect(editModal).toBeVisible();
// Verify input fields are visible
const titleInput = page.getByTestId('song-edit-modal-title-input');
const ccliInput = page.getByTestId('song-edit-modal-ccli-input');
const copyrightTextarea = page.getByTestId('song-edit-modal-copyright-textarea');
await expect(titleInput).toBeVisible();
await expect(ccliInput).toBeVisible();
await expect(copyrightTextarea).toBeVisible();
// Verify labels are visible
await expect(page.getByText('Titel')).toBeVisible();
await expect(page.getByText('CCLI-ID')).toBeVisible();
await expect(page.getByText('Copyright-Text')).toBeVisible();
});
// Test 3: Fields are auto-saved on change (debounced) — verify no explicit save button
test('modal fields auto-save on change without explicit save button', async ({ page }) => {
await page.goto('/songs');
await page.waitForLoadState('networkidle');
// Check if songs exist
const songTable = page.locator('table');
const hasTable = await songTable.isVisible().catch(() => false);
if (!hasTable) {
test.skip();
}
// Get first song row and click edit
const firstRow = page.locator('tbody tr').first();
const hasEditButton = await firstRow.getByTestId('song-list-edit-button').isVisible().catch(() => false);
if (!hasEditButton) {
test.skip();
}
const editButton = firstRow.getByTestId('song-list-edit-button');
await editButton.click();
// Wait for modal to appear
await page.waitForTimeout(300);
// Verify modal is visible
const editModal = page.getByTestId('song-edit-modal');
await expect(editModal).toBeVisible();
// Verify there is NO explicit save button
const saveButton = page.locator('button:has-text("Speichern")');
const saveButtonExists = await saveButton.isVisible().catch(() => false);
expect(saveButtonExists).toBe(false);
// Verify auto-save indicator exists (shows "Speichert…" or "Gespeichert")
const titleInput = page.getByTestId('song-edit-modal-title-input');
const currentValue = await titleInput.inputValue();
// Type a character to trigger auto-save
await titleInput.fill(currentValue + 'X');
// Wait for debounce (500ms) + save request
await page.waitForTimeout(700);
// Verify save indicator appears (either "Speichert…" or "Gespeichert")
const savingIndicator = page.getByText(/Speichert…|Gespeichert/);
const indicatorVisible = await savingIndicator.isVisible().catch(() => false);
expect(indicatorVisible).toBe(true);
// Restore original value (remove the 'X')
await titleInput.fill(currentValue);
await page.waitForTimeout(700);
});
// Test 4: Arrangement configurator is embedded in modal
test('arrangement configurator is embedded in modal', async ({ page }) => {
await page.goto('/songs');
await page.waitForLoadState('networkidle');
// Check if songs exist
const songTable = page.locator('table');
const hasTable = await songTable.isVisible().catch(() => false);
if (!hasTable) {
test.skip();
}
// Get first song row and click edit
const firstRow = page.locator('tbody tr').first();
const hasEditButton = await firstRow.getByTestId('song-list-edit-button').isVisible().catch(() => false);
if (!hasEditButton) {
test.skip();
}
const editButton = firstRow.getByTestId('song-list-edit-button');
await editButton.click();
// Wait for modal to appear
await page.waitForTimeout(300);
// Verify modal is visible
const editModal = page.getByTestId('song-edit-modal');
await expect(editModal).toBeVisible();
// Verify arrangement configurator section is visible
await expect(page.getByText('Arrangements')).toBeVisible();
// Verify arrangement configurator component is rendered
const arrangementConfigurator = page.getByTestId('arrangement-configurator');
const configuratorVisible = await arrangementConfigurator.isVisible().catch(() => false);
expect(configuratorVisible).toBe(true);
});
// Test 5: Close modal with X button
test('close modal with X button', async ({ page }) => {
await page.goto('/songs');
await page.waitForLoadState('networkidle');
// Check if songs exist
const songTable = page.locator('table');
const hasTable = await songTable.isVisible().catch(() => false);
if (!hasTable) {
test.skip();
}
// Get first song row and click edit
const firstRow = page.locator('tbody tr').first();
const hasEditButton = await firstRow.getByTestId('song-list-edit-button').isVisible().catch(() => false);
if (!hasEditButton) {
test.skip();
}
const editButton = firstRow.getByTestId('song-list-edit-button');
await editButton.click();
// Wait for modal to appear
await page.waitForTimeout(300);
// Verify modal is visible
const editModal = page.getByTestId('song-edit-modal');
await expect(editModal).toBeVisible();
// Click close button
const closeButton = page.getByTestId('song-edit-modal-close-button');
await closeButton.click();
// Wait for modal to close
await page.waitForTimeout(300);
// Verify modal is gone
const modalGone = await editModal.isVisible().catch(() => false);
expect(modalGone).toBe(false);
});
// Test 6: Close modal with overlay click
test('close modal with overlay click', async ({ page }) => {
await page.goto('/songs');
await page.waitForLoadState('networkidle');
// Check if songs exist
const songTable = page.locator('table');
const hasTable = await songTable.isVisible().catch(() => false);
if (!hasTable) {
test.skip();
}
// Get first song row and click edit
const firstRow = page.locator('tbody tr').first();
const hasEditButton = await firstRow.getByTestId('song-list-edit-button').isVisible().catch(() => false);
if (!hasEditButton) {
test.skip();
}
const editButton = firstRow.getByTestId('song-list-edit-button');
await editButton.click();
// Wait for modal to appear
await page.waitForTimeout(300);
// Verify modal is visible
const editModal = page.getByTestId('song-edit-modal');
await expect(editModal).toBeVisible();
// Click on the overlay (outside the modal)
const overlay = page.locator('div').filter({
has: editModal,
}).first();
// Get the overlay's bounding box and click outside the modal
const modalBox = await editModal.boundingBox();
if (modalBox) {
// Click on the left side of the overlay (outside modal)
await page.click('div[class*="fixed"][class*="inset-0"]', {
position: { x: 10, y: 10 },
});
}
// Wait for modal to close
await page.waitForTimeout(300);
// Verify modal is gone
const modalGone = await editModal.isVisible().catch(() => false);
expect(modalGone).toBe(false);
});