Kuma/tests/onboarding/onboarding-wizard-view.test.tsx
2026-10-01 23:54:20 +02:00

119 lines
4.6 KiB
TypeScript

import "@testing-library/jest-dom/vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import OnboardingWizardView from "@/src/modules/onboarding/ui/views/onboarding-wizard-view";
import {
ONBOARDING_STEPS,
WIZARD_STEP_IDS,
type OnboardingStepId,
} from "@/src/modules/onboarding/lib/onboarding-steps";
// Server Actions und Next-Navigation wegmocken - hier geht es ausschließlich um
// die Render-Stabilität des Wizards beim Durchklicken, nicht um die Fachlogik.
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }),
}));
vi.mock("@/src/modules/settings/actions/business-branding-actions", () => ({
saveBusinessBranding: vi.fn(async () => ({ success: true })),
}));
vi.mock("@/src/modules/settings/actions/cancellation-settings-actions", () => ({
saveCancellationPolicy: vi.fn(async () => ({ success: true })),
}));
vi.mock("@/src/modules/settings/actions/stripe-connect-actions", () => ({
createConnectOnboardingLink: vi.fn(async () => ({ success: true, url: "https://stripe.test" })),
}));
vi.mock("@/src/modules/rooms/actions/room-actions", () => ({
createRoom: vi.fn(async () => ({ success: true, message: "ok" })),
}));
vi.mock("@/src/modules/courses/actions/course-actions", () => ({
toggleBusinessPublic: vi.fn(async () => ({ success: true, slug: "test-studio" })),
}));
const allOpen = Object.fromEntries(
Object.keys(ONBOARDING_STEPS).map((id) => [id, false])
) as Record<OnboardingStepId, boolean>;
const defaultProps = {
businessId: "b1",
businessName: "Test Studio",
managerFirstName: "Alex",
done: allOpen,
branding: {
companyName: "Test Studio",
logoUrl: null,
primaryColor: "#1d3557",
accentColor: "#c9a84c",
},
policy: { enabled: true, mode: "tiered" as const, fixedRefundPercent: 100, tiers: [] },
isPublic: false,
slug: null,
stripeAccountStatus: null,
initialStep: null,
};
beforeEach(() => {
// jsdom kennt window.scrollTo nicht - der Wizard scrollt bei jedem Schritt nach oben.
window.scrollTo = vi.fn();
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("OnboardingWizardView - Render-Stabilität", () => {
it("klickt ohne Hook-Fehler vom Willkommens-Screen bis zum Abschluss durch", async () => {
const user = userEvent.setup();
// Ein Hook-Order-Verstoß ("Rendered more hooks than during the previous render")
// wirft React während des Renders - der Test schlägt dann hier fehl.
render(<OnboardingWizardView {...defaultProps} />);
expect(screen.getByText(/Hallo Alex/)).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /Los geht/ }));
// Jeder Wizard-Schritt einmal vorwärts
for (let i = 0; i < WIZARD_STEP_IDS.length; i++) {
expect(
screen.getByText(`Schritt ${i + 1} von ${WIZARD_STEP_IDS.length}`)
).toBeInTheDocument();
const nextLabel = i === WIZARD_STEP_IDS.length - 1 ? /^Fertig$/ : /Weiter|Überspringen/;
await user.click(screen.getByRole("button", { name: nextLabel }));
}
expect(screen.getByText(/Grundeinrichtung erledigt/)).toBeInTheDocument();
});
it("übersteht Vor- und Zurücknavigation zwischen allen Screens", async () => {
const user = userEvent.setup();
render(<OnboardingWizardView {...defaultProps} initialStep="publicPage" />);
// Direkteinstieg beim letzten Schritt (wie beim Rücksprung von Stripe)
expect(
screen.getByText(`Schritt ${WIZARD_STEP_IDS.length} von ${WIZARD_STEP_IDS.length}`)
).toBeInTheDocument();
// Über die Punkte-Navigation quer durch alle Schritte springen und wieder zurück
for (const id of WIZARD_STEP_IDS) {
await user.click(screen.getByRole("button", { name: ONBOARDING_STEPS[id].label }));
}
for (const id of [...WIZARD_STEP_IDS].reverse()) {
await user.click(screen.getByRole("button", { name: ONBOARDING_STEPS[id].label }));
}
expect(screen.getByText("Schritt 1 von " + WIZARD_STEP_IDS.length)).toBeInTheDocument();
});
it("wechselt zwischen Abschluss-Screen und letztem Schritt hin und her", async () => {
const user = userEvent.setup();
render(<OnboardingWizardView {...defaultProps} initialStep="publicPage" />);
await user.click(screen.getByRole("button", { name: /^Fertig$/ }));
expect(screen.getByText(/Grundeinrichtung erledigt/)).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /^Zurück$/ }));
expect(
screen.getByText(`Schritt ${WIZARD_STEP_IDS.length} von ${WIZARD_STEP_IDS.length}`)
).toBeInTheDocument();
});
});