import { test, expect } from "@playwright/test"; import { randomUUID } from "node:crypto"; const reviews = [ { createdAt: "2026-01-01", title: "Hilfreiche Beratung", review: "Die Beratung hat mir im Training sehr geholfen.", rating: 4, }, { createdAt: "2026-01-02", title: "Sehr guter Ernährungsplan", review: "Der Plan lässt sich leicht in meinen Alltag integrieren.", rating: 5, }, { createdAt: "2026-01-03", title: "Gute Tipps für den Alltag", review: "Ich konnte viele Tipps direkt im Alltag ausprobieren.", rating: 3, }, ]; const active = (page) => page.locator("#reviewSlider .review-card.is-active"); async function mock(page, rows = reviews) { await page.route("**/api/reviews", (route) => route.fulfill({ json: { reviews: rows } }), ); await page.goto("/index.html"); await expect(active(page)).toContainText( rows[0]?.title || "Deine Erfahrung", ); } test("10-Sekunden-Wechsel, Maus-Drag und Tastatur ohne Buttons", async ({ page, }) => { await page.clock.install({ time: new Date("2026-01-01T00:00:00Z") }); await page.clock.pauseAt(new Date("2026-01-01T00:00:01Z")); await mock(page); const stage = page.locator(".review-stage"); await expect(page.locator("#reviewSlider button")).toHaveCount(0); const font = await page .locator("body") .evaluate((element) => getComputedStyle(element).fontFamily); await expect(active(page).locator("h3")).toHaveCSS("font-family", font); await expect(active(page).locator("blockquote")).toHaveCSS( "font-family", font, ); await expect(active(page).locator(".stars")).toHaveAttribute( "aria-label", "4 von 5 Sternen", ); await page.clock.runFor(9999); await expect(active(page)).toContainText(reviews[0].title); await page.clock.runFor(1); await expect(active(page)).toContainText(reviews[1].title); await stage.scrollIntoViewIfNeeded(); const box = await stage.boundingBox(); await page.mouse.move(box.x + box.width / 2, box.y + 60); await page.mouse.down(); await page.mouse.move(box.x + box.width / 2 - 100, box.y + 60, { steps: 6, }); await expect(stage).toHaveClass(/is-dragging/); await expect(active(page)).toHaveCSS( "transform", "matrix(1, 0, 0, 1, -100, 0)", ); await page.mouse.up(); await expect(active(page)).toContainText(reviews[2].title); await expect(stage).toBeFocused(); await page.clock.runFor(9999); await expect(active(page)).toContainText(reviews[2].title); await page.clock.runFor(1); await expect(active(page)).toContainText(reviews[0].title); await page.keyboard.press("ArrowLeft"); await expect(active(page)).toContainText(reviews[2].title); await page.keyboard.press("Home"); await expect(active(page)).toContainText(reviews[0].title); await page.keyboard.press("End"); await expect(active(page)).toContainText(reviews[2].title); await page.keyboard.press("ArrowRight"); await expect(active(page)).toContainText(reviews[0].title); await page.clock.runFor(10_000); await expect(active(page)).toContainText(reviews[1].title); }); test("Kurzes Ziehen springt zurück; Drag nach rechts zeigt vorherige Rezension", async ({ page, }) => { await mock(page); const stage = page.locator(".review-stage"); await stage.scrollIntoViewIfNeeded(); const box = await stage.boundingBox(); for (const distance of [20, 100]) { await page.mouse.move(box.x + box.width / 2, box.y + 60); await page.mouse.down(); await page.mouse.move(box.x + box.width / 2 + distance, box.y + 60, { steps: 5, }); await page.mouse.up(); await expect(active(page)).toContainText( reviews[distance === 20 ? 0 : 2].title, ); await expect(stage).not.toHaveClass(/is-dragging/); } }); test("Leere Liste und einzelne Rezension brauchen keine Slider-Steuerung", async ({ page, }) => { await mock(page, []); await expect(page.locator("#reviewSlider button")).toHaveCount(0); await page.unroute("**/api/reviews"); await mock(page, [reviews[0]]); await expect(page.locator("#reviewSlider button")).toHaveCount(0); }); test("Mobile Anzeige bleibt stabil; HTML im Rezensionstext wird nicht ausgeführt", async ({ page, }) => { await page.setViewportSize({ width: 375, height: 850 }); await page.emulateMedia({ reducedMotion: "reduce" }); const rows = [ reviews[0], { ...reviews[1], review: '' + "x".repeat(400), }, ]; await mock(page, rows); const before = await page.locator(".review-stage").boundingBox(); await page.locator(".review-stage").focus(); await page.keyboard.press("ArrowRight"); const after = await page.locator(".review-stage").boundingBox(); expect(after.height).toBe(before.height); await expect(active(page)).toHaveCSS("transition-duration", "0s"); await expect(active(page)).toContainText(" document.documentElement.scrollWidth <= innerWidth, ), ).toBe(true); }); test("Neue Rezension erscheint erst nach erfolgreicher Speicherung und bleibt nach Neuladen", async ({ page, request, }) => { const email = `slider-${randomUUID()}@example.test`; expect( ( await request.post("/api/bookings", { headers: { "Idempotency-Key": randomUUID() }, data: { packageId: "ernaehrung-starter", name: "Slider Test", email, consent: true, }, }) ).status(), ).toBe(201); await page.goto("/index.html"); const title = `Neue Rezension ${randomUUID().slice(0, 8)}`; const form = page.locator("#reviewForm"); await form.locator('[name="email"]').fill("unbekannt@example.test"); await form.locator('[name="title"]').fill(title); await form .locator('[name="review"]') .fill("Eine neue und hilfreiche Erfahrung mit der Beratung."); await form.locator('label[for="star4"]').click(); await form.locator('[name="consent"]').check(); await form.locator('[type="submit"]').click(); await expect(page.locator("#formStatus")).toContainText( "keine Buchung gefunden", ); await expect(page.locator("#reviewSlider")).not.toContainText(title); await form.locator('[name="email"]').fill(email); await form.locator('[type="submit"]').click(); await expect(page.locator("#formStatus")).toContainText("gespeichert"); await expect(active(page)).toContainText(title); await expect(page.locator("#reviewSlider")).not.toContainText(email); await page.reload(); await expect(page.locator("#reviewSlider")).toContainText(title); }); test("Polling nimmt neue Backend-Rezensionen auf; Lesefehler erhalten vorhandene Karten", async ({ page, }) => { await page.clock.install({ time: new Date("2026-01-01T00:00:00Z") }); await page.clock.pauseAt(new Date("2026-01-01T00:00:01Z")); let rows = [reviews[0]]; let fail = false; await page.route("**/api/reviews", (route) => route.fulfill( fail ? { status: 503, json: {} } : { json: { reviews: rows } }, ), ); await page.goto("/index.html"); await expect(active(page)).toContainText(reviews[0].title); rows = reviews; await page.clock.runFor(10_000); await expect(page.locator("#reviewSlider .review-card")).toHaveCount(3); fail = true; await page.clock.runFor(10_000); await expect(page.locator(".review-feedback")).toContainText( "nicht aktualisiert", ); await expect(page.locator("#reviewSlider .review-card")).toHaveCount(3); });