import { test, expect } from "@playwright/test"; import { packages } from "../../shared/packages.js"; import { packagesDetails } from "../../shared/packagesdetails.js"; const pageUrl = "/booking.html#/buchen/ernaehrung-starter"; async function fill(page) { if (!(await page.locator("dialog[open]").count())) { await page .getByRole("button", { name: "Jetzt buchen", exact: true }) .click(); } await page .getByLabel("Name (Pflichtfeld)", { exact: true }) .fill("Test Person"); await page .getByLabel("E-Mail-Adresse (Pflichtfeld)") .fill("person@example.test"); await page.locator('[name="consent"]').check(); } test("Tastaturbedienung, Validierung und vollständige Buchung über Vite-Proxy", async ({ page, }) => { await page.goto(pageUrl); await expect( page.getByRole("heading", { name: packagesDetails[0].title }), ).toBeVisible(); await expect(page.locator(".booking__payment-notice")).toContainText( "Zahlungsvorgang wird übersprungen", ); await expect(page.locator(".booking__payment-notice")).toContainText( "keine Abbuchung", ); const opener = page.getByRole("button", { name: "Jetzt buchen", exact: true, }); await opener.focus(); await page.keyboard.press("Enter"); await expect( page.getByLabel("Name (Pflichtfeld)", { exact: true }), ).toBeFocused(); await page.keyboard.press("Enter"); await expect(page.getByRole("alert")).toBeFocused(); await expect(page.locator('[aria-invalid="true"]')).toHaveCount(3); await page.keyboard.press("Tab"); await page.keyboard.type("Test Person"); await page.keyboard.press("Tab"); await page.keyboard.type("person@example.test"); await page.keyboard.press("Tab"); await page.keyboard.press("Tab"); await expect(page.locator('[name="consent"]')).toBeFocused(); await page.keyboard.press("Space"); await page.keyboard.press("Tab"); await page.keyboard.press("Tab"); await page.keyboard.press("Tab"); const submit = page.getByRole("button", { name: "Paket buchen", exact: true, }); await expect(submit).toBeFocused(); expect( await submit.evaluate( (element) => getComputedStyle(element).outlineStyle, ), ).toBe("solid"); await page.keyboard.press("Enter"); await expect(page).toHaveURL(/booking-confirmation\.html$/); await expect(page.locator("#confirmation-receipt")).toContainText( /TH-\d{6,}/, ); await page.reload(); await expect(page.locator("#confirmation-receipt")).toContainText( /TH-\d{6,}/, ); }); test("Schmales Display: Formular ohne horizontalen Überlauf", async ({ page, }) => { await page.setViewportSize({ width: 320, height: 640 }); await page.goto(pageUrl); await fill(page); expect( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), ).toBe(true); await expect( page.getByRole("button", { name: "Paket buchen", exact: true }), ).toBeVisible(); }); test("Verlorene Antwort: Wiederholung nach Neuladen behält Schlüssel und Buchungsnummer", async ({ page, }) => { let firstKey; let firstBooking; let requests = 0; await page.route("**/api/bookings", async (route) => { requests++; if (requests === 1) { firstKey = route.request().headers()["idempotency-key"]; const response = await route.fetch(); firstBooking = (await response.json()).bookingId; await route.abort("failed"); } else { expect(route.request().headers()["idempotency-key"]).toBe(firstKey); await route.continue(); } }); await page.goto(pageUrl); await fill(page); await page .getByRole("button", { name: "Paket buchen", exact: true }) .click(); await expect(page.getByRole("alert")).toBeVisible(); await page.reload(); await page .getByRole("button", { name: "Jetzt buchen", exact: true }) .click(); await expect( page.getByLabel("Name (Pflichtfeld)", { exact: true }), ).toHaveValue("Test Person"); await expect( page.getByLabel("Name (Pflichtfeld)", { exact: true }), ).toHaveAttribute("readonly", ""); await page .getByRole("button", { name: "Status prüfen / erneut versuchen" }) .click(); await expect(page.locator("#confirmation-receipt")).toContainText( firstBooking, ); expect(requests).toBe(2); }); test("Ladezustand verhindert doppelte Klicks; Mailfehler behauptet keinen vollständigen Erfolg", async ({ page, }) => { let release; const gate = new Promise((resolve) => { release = resolve; }); await page.route("**/api/bookings", async (route) => { await gate; await route.fulfill({ status: 202, contentType: "application/json", body: JSON.stringify({ saved: true, bookingId: "test-mailfehler", emailStatus: "failed", }), }); }); await page.goto(pageUrl); await fill(page); await page .getByRole("button", { name: "Paket buchen", exact: true }) .click(); await expect( page.getByRole("button", { name: "Buchung wird verarbeitet …" }), ).toBeDisabled(); release(); await expect(page.locator(".booking__result")).toContainText( "Bestätigungsversand ist fehlgeschlagen", ); await expect(page.locator(".booking__result")).toContainText( "Bitte nicht erneut buchen", ); await expect( page.getByRole("button", { name: "Buchung gespeichert" }), ).toBeDisabled(); }); test("Fehlende und unbekannte Paket-ID zeigen kein Formular", async ({ page, }) => { await page.goto("/booking.html"); await expect(page.getByText("Bitte wähle zuerst ein Paket")).toBeVisible(); await expect(page.locator("form")).toHaveCount(0); await page.goto("/booking.html#/buchen/unbekannt"); await expect( page.getByText("Dieses Paket ist nicht verfügbar"), ).toBeVisible(); await expect(page.locator("form")).toHaveCount(0); }); test("Vite liefert Serverdateien und Umgebungsdateien nicht aus", async ({ request, }) => { for (const path of [ "/src/server/index.js", "/src/server/data/.gitkeep", "/.env.example", ]) { const response = await request.get(path); expect([403, 404]).toContain(response.status()); } }); for (const pkg of packages) { test(`Von der Startseite zur Buchung: ${pkg.type}`, async ({ page }) => { await page.goto("/"); await page .getByRole("link", { name: "Ernährungswissen entdecken" }) .click(); await expect(page.locator(".package-card")).toHaveCount( packages.length, ); await page.getByRole("link", { name: pkg.buttonText }).click(); await expect(page).toHaveURL( new RegExp(`paket-details\\.html\\?id=${pkg.id}$`), ); await page.locator("#proceed-to-booking-btn").click(); await expect(page.locator("dialog[open]")).toBeVisible(); const details = packagesDetails.find((entry) => entry.id === pkg.id); await expect(page.locator(".booking__package-name")).toHaveText( details.title, ); await expect(page.locator(".booking__summary")).toHaveText( details.summaryForBooking, ); await expect(page.locator(".booking__features li")).toHaveCount( details.includedFeatures.length, ); await fill(page); const responsePromise = page.waitForResponse( (response) => response.url().endsWith("/api/bookings") && response.request().method() === "POST", ); await page .getByRole("button", { name: "Paket buchen", exact: true }) .click(); const response = await responsePromise; expect(response.status()).toBe(201); await expect(page).toHaveURL(/booking-confirmation\.html$/); const booking = await page.evaluate(() => JSON.parse(sessionStorage.getItem("trihub.booking.confirmation")), ); expect(booking.packageId).toBe(pkg.id); expect(booking.packageName).toBe(pkg.name); await expect(page.locator("#confirmation-receipt")).toContainText( booking.bookingId, ); await page.goto("/angebotsuebersicht.html"); await expect(page.locator(".package-card")).toHaveCount( packages.length, ); }); } test("Angebotsübersicht passt auf ein schmales Display", async ({ page }) => { await page.setViewportSize({ width: 320, height: 640 }); await page.goto("/angebotsuebersicht.html"); await expect(page.locator(".package-card")).toHaveCount(packages.length); expect( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), ).toBe(true); }); test("Premium-Jahresvariante wird von der Detailseite bis zur Bestätigung übernommen", async ({ page, }) => { await page.goto("/paket-details.html?id=ernaehrung-premium"); await page.locator("#variant-select").selectOption("ernaehrung-premium-52"); await page.locator("#proceed-to-booking-btn").click(); await expect(page.locator("dialog[open]")).toBeVisible(); await expect(page.locator(".booking__totals")).toContainText( "52 Wochen Begleitung", ); await expect(page.locator(".booking__totals")).toContainText("1.399,00"); await fill(page); const responsePromise = page.waitForResponse( (response) => response.url().endsWith("/api/bookings") && response.request().method() === "POST", ); await page .getByRole("button", { name: "Paket buchen", exact: true }) .click(); const response = await responsePromise; expect(response.request().postDataJSON().variantId).toBe( "ernaehrung-premium-52", ); await expect(page).toHaveURL(/booking-confirmation\.html$/); const booking = await page.evaluate(() => JSON.parse(sessionStorage.getItem("trihub.booking.confirmation")), ); expect(booking.bookedPackage.grossPrice).toBe(1399); expect(booking.bookedPackage.durationWeeks).toBe(52); await expect(page.locator("#confirmation-receipt")).toContainText( booking.bookingId, ); await page.reload(); await expect(page.locator("#confirmation-totals")).toContainText( "1.399,00", ); await expect(page.locator("#confirmation-receipt")).toContainText( booking.bookingId, ); }); test("Paketfremde Variante zeigt kein Buchungsformular", async ({ page }) => { await page.goto( "/booking.html?id=ernaehrung-starter&variant=ernaehrung-premium-52", ); await expect( page.getByText("Diese Variante gehört nicht zum ausgewählten Paket."), ).toBeVisible(); await expect(page.locator("form")).toHaveCount(0); });