diff --git a/tests/booking-flow.spec.js b/tests/booking-flow.spec.js new file mode 100644 index 0000000..7bc4f37 --- /dev/null +++ b/tests/booking-flow.spec.js @@ -0,0 +1,255 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Booking Flow End-to-End', () => { + const ADMIN_URL = 'http://localhost:55581'; + const ADMIN_EMAIL = 'admin@mccars.local'; + const ADMIN_PASSWORD = 'mc-cars-admin'; + + // Generate unique test data per run to avoid conflicts + const ts = Date.now(); + const testEmails = [ + `test-day-${ts}@playwright.test`, + `test-weekend-${ts}@playwright.test`, + `test-custom-${ts}@playwright.test`, + ]; + const testNames = [ + 'Test Testerson Day', + 'Test Testerson Weekend', + 'Test Testerson Custom', + ]; + + /** + * Helper: fill out the booking form for a given mietdauer type. + * Returns nothing - the form submission is handled by the page's JS. + */ + async function submitBooking(page, type, index) { + // Scroll to booking section + await page.locator('#buchen').scrollIntoViewIfNeeded(); + await page.waitForTimeout(500); + + // Step 1: Select vehicle + const carSelect = page.locator('#bpfCar'); + await expect(carSelect).toBeVisible({ timeout: 10000 }); + // Select first available vehicle option (skip the placeholder) + const options = await carSelect.locator('option').all(); + expect(options.length).toBeGreaterThan(1); + const firstVehicle = await options[1].innerText(); + await carSelect.selectOption({ label: firstVehicle }); + + // Step 2: Select mietdauer type + const presetBtn = page.locator(`.bpf-preset[data-preset="${type}"]`); + await expect(presetBtn).toBeVisible(); + await presetBtn.click(); + + // Step 3: Pick date(s) based on type + if (type === 'day') { + // Pick a date 7 days from now + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + const dateStr = futureDate.toISOString().split('T')[0]; + const dateInput = page.locator('#bpfDayDate'); + await dateInput.fill(dateStr); + } else if (type === 'weekend') { + // Pick next Saturday + const nextSaturday = new Date(); + const daysUntilSaturday = (6 - nextSaturday.getDay() + 7) % 7 || 7; + nextSaturday.setDate(nextSaturday.getDate() + daysUntilSaturday); + const dateStr = nextSaturday.toISOString().split('T')[0]; + const dateInput = page.locator('#bpfWeekendDate'); + await dateInput.fill(dateStr); + } else if (type === 'custom') { + // Pick start date 14 days from now, end date 17 days from now (4 days = individuell) + const startDate = new Date(); + startDate.setDate(startDate.getDate() + 14); + const endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + 3); + const fromStr = startDate.toISOString().split('T')[0]; + const toStr = endDate.toISOString().split('T')[0]; + await page.locator('#bpfFrom').fill(fromStr); + await page.locator('#bpfTo').fill(toStr); + } + + // Click Weiter to go to step 2 + await page.locator('#bpfNext1').click(); + await page.waitForTimeout(300); + + // Step 2: Fill contact info + await expect(page.locator('#bpfName')).toBeVisible(); + await page.locator('#bpfName').fill(testNames[index]); + await page.locator('#bpfEmail').fill(testEmails[index]); + await page.locator('#bpfPhone').fill('+43 660 1234567'); + await page.locator('#bpfMessage').fill(`Test booking via playwright - ${type}`); + + // Click Weiter to go to step 3 + await page.locator('#bpfNext2').click(); + await page.waitForTimeout(300); + + // Step 3: Submit (skip file uploads - they are optional) + await expect(page.locator('#bpfSubmit')).toBeVisible(); + await page.locator('#bpfSubmit').click(); + + // Wait for success toast + await expect(page.locator('#toast.show')).toBeVisible({ timeout: 10000 }); + await page.waitForTimeout(1000); + } + + test('Complete booking flow: 1 Tag, Wochenende, Individuell → 3 leads in admin → disqualify all', async ({ page, context }) => { + // ======================================== + // PART 1: Submit 3 bookings on main site + // ======================================== + await page.goto('/'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(2000); + + // Booking 1: 1 Tag + await submitBooking(page, 'day', 0); + + // Booking 2: Wochenende + await submitBooking(page, 'weekend', 1); + + // Booking 3: Individuell + await submitBooking(page, 'custom', 2); + + // ======================================== + // PART 2: Verify 3 leads in admin panel + // ======================================== + const adminCtx = await test.info().project.use.baseBrowserType?.newContext() ?? context; + const adminPage = await adminCtx.newPage(); + adminPage.setDefaultTimeout(30000); + + await adminPage.goto(ADMIN_URL); + await adminPage.waitForLoadState('domcontentloaded'); + await adminPage.waitForTimeout(2000); + + // Login + const loginForm = adminPage.locator('#loginForm'); + await expect(loginForm).toBeVisible({ timeout: 10000 }); + await adminPage.locator('#loginForm [name="email"]').fill(ADMIN_EMAIL); + await adminPage.locator('#loginForm [name="password"]').fill(ADMIN_PASSWORD); + await adminPage.locator('#loginForm [type="submit"]').click(); + + // Wait a moment for login to process + await adminPage.waitForTimeout(3000); + + // Check for login error + const loginError = adminPage.locator('#loginError'); + if (await loginError.isVisible()) { + const errorMsg = await loginError.textContent(); + throw new Error(`Login failed: ${errorMsg}`); + } + + // Check if password rotation is required (first login) + const rotateView = adminPage.locator('#rotateView'); + if (await rotateView.isVisible({ timeout: 2000 })) { + // Set a new password (must be different from bootstrap) + const newPw = 'Playwright-Test-PW-2026!'; + await adminPage.locator('#rotateForm [name="pw1"]').fill(newPw); + await adminPage.locator('#rotateForm [name="pw2"]').fill(newPw); + await adminPage.locator('#rotateForm [type="submit"]').click(); + await adminPage.waitForTimeout(2000); + } + + // Wait for admin view to load + await expect(adminPage.locator('#adminView')).toBeVisible({ timeout: 15000 }); + await adminPage.waitForTimeout(2000); + + // Ensure leads tab is active (it's the default) + const leadsTab = adminPage.locator('[data-tab="leads"]'); + const leadsTabClass = await leadsTab.getAttribute('class'); + if (!leadsTabClass?.includes('active')) { + await leadsTab.click(); + await adminPage.waitForTimeout(1000); + } + + // Wait for our test leads to appear by checking for their emails in the table + // We wait for at least one of our test emails to appear, then verify all 3 + await adminPage.waitForFunction( + ([emails]) => { + const rows = document.querySelectorAll('#leadsTable tbody tr'); + let found = 0; + for (const row of rows) { + const text = row.textContent; + for (const email of emails) { + if (text.includes(email)) { + found++; + break; + } + } + } + return found >= 3; + }, + testEmails, + { timeout: 30000 } + ); + + await adminPage.waitForTimeout(1000); + + // Find our test leads by email pattern + const allRows = adminPage.locator('#leadsTable tbody tr'); + const totalRows = await allRows.count(); + const testRowIndices = []; + + for (let i = 0; i < totalRows; i++) { + const row = allRows.nth(i); + const rowText = await row.textContent(); + if (testEmails.some(email => rowText.includes(email))) { + testRowIndices.push(i); + } + } + + expect(testRowIndices.length).toBe(3); + + // ======================================== + // PART 3: Disqualify all 3 test leads + // ======================================== + // Disqualify each lead one at a time, re-finding it after each disqualification + // since the table re-renders and indices shift. + for (const email of testEmails) { + // Find the row for this email + const rows = adminPage.locator('#leadsTable tbody tr'); + const count = await rows.count(); + let found = false; + + for (let i = 0; i < count; i++) { + const rowText = await rows.nth(i).textContent(); + if (rowText.includes(email)) { + // Click disqualify button + const disqBtn = rows.nth(i).locator('[data-disq]'); + if (await disqBtn.isVisible()) { + await disqBtn.click(); + await adminPage.waitForTimeout(1500); + found = true; + break; + } + } + } + + expect(found).toBe(true, `Lead with email ${email} not found or could not be disqualified`); + } + + // Wait for disqualifications to process + await adminPage.waitForTimeout(2000); + + // Refresh page to ensure fresh data after disqualifications + await adminPage.reload(); + await expect(adminPage.locator('#adminView')).toBeVisible({ timeout: 15000 }); + await adminPage.waitForTimeout(3000); + + // Verify our test leads are now disqualified (no longer in active view) + const remainingRows = adminPage.locator('#leadsTable tbody tr'); + const remainingCount = await remainingRows.count(); + let foundTestLead = false; + + for (let i = 0; i < remainingCount; i++) { + const rowText = await remainingRows.nth(i).textContent(); + if (testEmails.some(email => rowText.includes(email))) { + foundTestLead = true; + break; + } + } + + expect(foundTestLead).toBe(false); + + await adminPage.close(); + }); +});