Kuma/tests/courses/business-public-view.test.tsx
2026-10-01 23:54:20 +02:00

208 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
const nav = vi.hoisted(() => ({ params: new URLSearchParams() }));
vi.mock("next/navigation", () => ({ useSearchParams: () => nav.params }));
vi.mock("next/image", () => ({ default: () => null }));
// Block (Desktop) und Zeitleiste (Handy) sind eigene Bausteine - hier zählt,
// welche Kurse in welcher Woche bzw. an welchem Tag erscheinen.
vi.mock("@/src/modules/courses/ui/components/CalendarEventBlock", () => ({
default: ({ course }: { course: { name: string; timeFrom: string } }) => (
<div data-testid="card">{`${course.timeFrom} ${course.name}`}</div>
),
}));
vi.mock("@/src/modules/courses/ui/components/DayTimeline", () => ({
default: ({ courses }: { courses: { id: string; name: string; timeFrom: string }[] }) => (
<ol>
{courses.map((course) => (
<li key={course.id} data-testid="row">{`${course.timeFrom} ${course.name}`}</li>
))}
</ol>
),
}));
vi.mock("@/src/modules/courses/ui/components/BookingDialog", () => ({
BookingDialog: ({ open, course }: { open: boolean; course: { name: string } }) =>
open ? <div role="dialog">{course.name}</div> : null,
}));
vi.mock("@/src/modules/courses/actions/booking-actions", () => ({
getPublishedCoursesForBusiness: vi.fn(),
}));
import BusinessPublicView from "@/src/modules/courses/ui/views/business-public-view";
import { getPublishedCoursesForBusiness } from "@/src/modules/courses/actions/booking-actions";
const business = {
id: "biz-1",
name: "Since3",
address: "Sandweg 91, 60316 Frankfurt",
email: "studio@example.com",
title: "Since3",
slug: "since3",
logoUrl: null,
primaryColor: "#1e293b",
accentColor: "#f4c00c",
};
// Mitternacht Berliner Zeit, so wie Kurse in der DB liegen.
const DAY = {
thu: "2026-09-30T22:00:00Z", // heute, Donnerstag 1. Oktober
fri: "2026-10-01T22:00:00Z",
oct14: "2026-10-13T22:00:00Z",
oct29: "2026-10-28T23:00:00Z",
};
function course(id: string, name: string, date: string, timeFrom: string) {
return {
id,
name,
sport: [name.toLowerCase().split(" ")[0]],
level: "any",
date: new Date(date),
timeFrom,
timeTo: "23:59",
maxParticipants: 10,
price: 12,
currentParticipants: 3,
};
}
const thisWeek = [
course("started", "Frueh", DAY.thu, "07:30"),
course("tonight", "Yoga Flow", DAY.thu, "18:00"),
course("fri", "Spinning", DAY.fri, "07:30"),
];
const later = [
course("yoga", "Yoga", DAY.oct14, "18:00"),
course("pilates", "Reformer Pilates", DAY.oct29, "18:00"),
];
const window = { from: new Date("2026-09-30T08:00:00Z"), to: new Date("2026-10-31T08:00:00Z") };
function renderView(initialCourses = [...thisWeek, ...later], initialWindow = window) {
return render(
<BusinessPublicView
business={business}
initialCourses={initialCourses}
initialWindow={initialWindow}
serviceFeeCents={50}
cancellationSummary="Kostenlos stornierbar bis Kursbeginn."
/>
);
}
const cards = () => screen.queryAllByTestId("card").map((c) => c.textContent);
const rows = () => screen.queryAllByTestId("row").map((r) => r.textContent);
beforeEach(() => {
// Donnerstag, 1. Oktober 2026, 10:00 Uhr in Berlin. Nur Date fälschen.
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date("2026-10-01T08:00:00Z"));
vi.mocked(getPublishedCoursesForBusiness).mockReset();
nav.params = new URLSearchParams();
});
afterEach(() => {
vi.useRealTimers();
});
describe("Öffentliche Studio-Seite – Wochenkalender", () => {
it("zeigt die aktuelle Woche ohne bereits begonnene Kurse", () => {
renderView();
expect(cards()).toEqual(["18:00 Yoga Flow", "07:30 Spinning"]);
expect(screen.getByRole("button", { name: "Vorherige Woche" })).toHaveProperty(
"disabled",
true
);
});
it("zeigt neue Kurse in zwei Wochen sofort - der Kalender startet in ihrer Woche", () => {
// Der gemeldete Fall: Kurse am 14. und 29.10. waren hinter "Nächste 7 Tage" versteckt.
renderView(later);
expect(cards()).toEqual(["18:00 Yoga"]);
});
it("führt aus einer leeren Woche zum nächsten Kurs", async () => {
// Die Woche des 29.10. reicht über das geladene Fenster hinaus - nachladen.
vi.mocked(getPublishedCoursesForBusiness).mockResolvedValue({
success: true,
courses: [],
} as never);
renderView(later);
fireEvent.click(screen.getByRole("button", { name: "Nächste Woche" }));
expect(cards()).toEqual([]);
expect(screen.getByText("In dieser Woche finden keine Kurse statt.")).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: /Nächste Kurse ab .*29\. Oktober/ }));
expect((await screen.findAllByTestId("card")).map((c) => c.textContent)).toEqual([
"18:00 Reformer Pilates",
]);
});
it("zeigt am Handy die Kurse des gewählten Tages", () => {
renderView();
expect(rows()).toEqual(["18:00 Yoga Flow"]);
fireEvent.click(screen.getByRole("button", { name: /^Morgen · Freitag/ }));
expect(rows()).toEqual(["07:30 Spinning"]);
});
it("zeigt in der Kartenansicht dieselbe Woche als Karten", () => {
renderView();
fireEvent.click(screen.getByRole("button", { name: "Karten" }));
expect(cards()).toEqual([]);
const titles = screen.getAllByRole("heading", { level: 4 }).map((h) => h.textContent);
expect(titles).toEqual(["Yoga Flow", "Spinning"]);
expect(screen.getByRole("region", { name: /^Morgen · Freitag/ })).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Kalender" }));
expect(cards()).toEqual(["18:00 Yoga Flow", "07:30 Spinning"]);
});
it("lädt nach, wenn eine Woche hinter dem Geladenen liegt", () => {
vi.mocked(getPublishedCoursesForBusiness).mockResolvedValue({
success: true,
courses: [],
} as never);
const shortWindow = { from: window.from, to: new Date("2026-10-08T08:00:00Z") };
renderView(thisWeek, shortWindow);
fireEvent.click(screen.getByRole("button", { name: "Nächste Woche" }));
expect(getPublishedCoursesForBusiness).toHaveBeenCalledWith("biz-1", {
from: shortWindow.to,
to: new Date(shortWindow.to.getTime() + 30 * 24 * 60 * 60 * 1000),
});
});
it("öffnet einen geteilten Link auf einen Kurs in einer späteren Woche", () => {
nav.params = new URLSearchParams({ course: "pilates" });
renderView();
expect(screen.getByRole("dialog").textContent).toBe("Reformer Pilates");
// Der Kalender steht dahinter in der Woche des Kurses.
expect(cards()).toEqual(["18:00 Reformer Pilates"]);
});
it("öffnet keinen Buchungsdialog für einen bereits begonnenen Kurs", () => {
// "Frueh" lief heute um 07:30 - jetzt ist es 10:00.
nav.params = new URLSearchParams({ course: "started" });
renderView();
expect(screen.queryByRole("dialog")).toBeNull();
expect(screen.getByRole("status").textContent).toMatch(
/„Frueh“ hat bereits begonnen und kann nicht mehr gebucht werden/
);
expect(cards()).toEqual(["18:00 Yoga Flow", "07:30 Spinning"]);
});
it("nennt Stornoregel und Servicegebühr in einer Zeile", () => {
renderView();
expect(
screen.getByText(
/Kostenlos stornierbar bis Kursbeginn\.\s+Alle Preise inkl\. 0,50\s€ Servicegebühr\./
)
).toBeTruthy();
});
});