mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
208 lines
7.3 KiB
TypeScript
208 lines
7.3 KiB
TypeScript
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();
|
||
});
|
||
});
|