218 lines
7.9 KiB
JavaScript
218 lines
7.9 KiB
JavaScript
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:
|
|
'<img src=x onerror="window.reviewInjected=true">' +
|
|
"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("<img src=x");
|
|
await expect(active(page).locator("img")).toHaveCount(0);
|
|
expect(
|
|
await page.evaluate(
|
|
() => 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);
|
|
});
|