Files
calendar/apps/web/modules/ee/organizations/lib/onboardingStore.test.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
0774a41119 refactor: move hooks and stores from packages/features to apps/web/modules (#27221)
* refactor: move booking-audit client components from packages/features to apps/web/modules

This is part of the larger effort to move tRPC-dependent UI components from packages/features to apps/web/modules to eliminate circular dependencies.

Changes:
- Move BookingHistory.tsx and BookingHistoryPage.tsx to apps/web/modules/booking-audit/components/
- Update imports in apps/web to use the new location

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move formbricks client from packages/features to apps/web/modules

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move hooks and stores from packages/features to apps/web/modules

- Move useAppsData hook from packages/features/apps/hooks to apps/web/modules/apps/hooks
- Move onboardingStore from packages/features/ee/organizations/lib to apps/web/modules/ee/organizations/lib
- Move useWelcomeModal hook from packages/features/ee/organizations/hooks to apps/web/modules/ee/organizations/hooks
- Move useAgentsData hook from packages/features/ee/workflows/hooks to apps/web/modules/ee/workflows/hooks
- Update all import paths in consuming files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move onboardingStore test file to apps/web/modules

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-26 13:11:14 +00:00

395 lines
12 KiB
TypeScript

/**
* @vitest-environment jsdom
*/
// @ts-nocheck - Test file with mock type compatibility issues that don't affect test functionality
import { renderHook, act, waitFor } from "@testing-library/react";
import { useSession } from "next-auth/react";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { useOnboardingStore, useOnboarding } from "./onboardingStore";
// Mock all dependencies
vi.mock("next-auth/react");
vi.mock("next/navigation");
vi.mock("@calcom/trpc/react", () => ({
trpc: {
viewer: {
organizations: {
getOrganizationOnboarding: {
useQuery: vi.fn(),
},
},
},
},
}));
// Mock constants
vi.mock("@calcom/lib/constants", async () => {
const actual = await vi.importActual("@calcom/lib/constants");
return {
...actual,
IS_SELF_HOSTED: false, // Default to false, will be overridden in specific tests
};
});
function renderUseOnboardingStore() {
renderHook(() => useOnboarding(), { wrapper: createWrapper() });
return renderHook(() => useOnboardingStore(), { wrapper: createWrapper() });
}
// Test Data Builders
const createTestSession = (overrides?: {
email?: string;
role?: UserPermissionRole;
status?: "loading" | "authenticated" | "unauthenticated";
}) => {
const status = overrides?.status || "authenticated";
if (status === "loading") {
return {
data: null,
status: "loading" as const,
update: vi.fn(),
};
}
if (status === "unauthenticated") {
return {
data: null,
status: "unauthenticated" as const,
update: vi.fn(),
};
}
return {
data: {
user: {
email: overrides?.email || "test@example.com",
role: overrides?.role || UserPermissionRole.USER,
},
},
status: "authenticated" as const,
update: vi.fn(),
};
};
const createTestOrganizationOnboarding = (overrides?: {
isComplete?: boolean;
id?: string;
billingPeriod?: string;
pricePerSeat?: number;
seats?: number;
orgOwnerEmail?: string;
name?: string;
slug?: string;
bio?: string;
logo?: string;
}) => ({
id: overrides?.id || "test-onboarding-id",
isComplete: overrides?.isComplete ?? false,
billingPeriod: overrides?.billingPeriod || "MONTHLY",
pricePerSeat: overrides?.pricePerSeat ?? 20,
seats: overrides?.seats ?? 5,
orgOwnerEmail: overrides?.orgOwnerEmail || "test@example.com",
name: overrides?.name || "Test Organization",
slug: overrides?.slug || "test-org",
bio: overrides?.bio || "Test bio",
logo: overrides?.logo || "test-logo.png",
});
function mockOrganizationOnboardingFromDB(data: ReturnType<typeof createTestOrganizationOnboarding> | null) {
const organizationOnboarding = data ? createTestOrganizationOnboarding(data) : null;
(trpc.viewer.organizations.getOrganizationOnboarding.useQuery as ReturnType<typeof vi.fn>).mockReturnValue({
data: organizationOnboarding,
isPending: false,
isLoading: false,
error: null,
refetch: vi.fn(),
});
}
const createTestSearchParams = (params?: Record<string, string>) => {
return new URLSearchParams(params);
};
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
Object.defineProperty(window, "localStorage", {
value: localStorageMock,
writable: true,
});
// Mock implementations
const mockRouterPush = vi.fn().mockImplementation((...args: string[]) => {
console.trace();
console.log("mockRouterPush called", args[0]);
});
const mockUseSession = vi.mocked(useSession);
vi.mocked(useRouter).mockReturnValue({
push: mockRouterPush,
replace: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
prefetch: vi.fn(),
});
const mockUsePathname = vi.mocked(usePathname);
const mockUseSearchParams = vi.mocked(useSearchParams);
// Test wrapper for React hooks - no wrapper needed for this test
const createWrapper = () => undefined;
describe("OnboardingStore", () => {
beforeEach(() => {
// Reset the store and let React do any side effects
act(() => {
useOnboardingStore.getState().reset();
});
// Reset localStorage mock
localStorageMock.clear.mockClear();
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
localStorageMock.removeItem.mockClear();
mockRouterPush.mockClear();
// Setup default mocks
mockUsePathname.mockReturnValue("/settings/organizations/new");
mockUseSearchParams.mockReturnValue(createTestSearchParams());
(
trpc.viewer.organizations.getOrganizationOnboarding.useQuery as ReturnType<typeof vi.fn>
).mockReturnValue({
data: undefined,
isPending: false,
isLoading: false,
error: null,
refetch: vi.fn(),
});
});
afterEach(() => {
act(() => {
useOnboardingStore.getState().reset();
});
vi.clearAllMocks();
mockRouterPush.mockClear();
localStorageMock.clear.mockClear();
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
localStorageMock.removeItem.mockClear();
});
describe("Zustand Store Actions - Complex Logic", () => {
describe("Reset Functionality", () => {
it("should reset to initial state when called without parameters", () => {
const { result } = renderHook(() => useOnboardingStore(), { wrapper: createWrapper() });
// Set some values
act(() => {
result.current.setName("Test Org");
result.current.setSlug("test-org");
result.current.setBillingPeriod("ANNUALLY" as const);
result.current.setSeats(15);
result.current.addInvitedMember({ email: "test@example.com", name: "Test User" });
});
// Reset
act(() => {
result.current.reset();
});
expect(result.current.name).toBe("");
expect(result.current.slug).toBe("");
expect(result.current.billingPeriod).toBeUndefined();
expect(result.current.seats).toBeNull();
expect(result.current.invitedMembers).toEqual([]);
});
it("should set specific state when called with parameters", () => {
const { result } = renderHook(() => useOnboardingStore(), { wrapper: createWrapper() });
const newState = {
name: "Partial Reset Org",
slug: "partial-reset",
seats: 25,
};
act(() => {
result.current.reset(newState);
});
expect(result.current.name).toBe("Partial Reset Org");
expect(result.current.slug).toBe("partial-reset");
expect(result.current.seats).toBe(25);
});
it("should clear localStorage when resetting without parameters", () => {
const { result } = renderHook(() => useOnboardingStore(), { wrapper: createWrapper() });
// Set some values to trigger localStorage save
act(() => {
result.current.setName("Test Org");
});
// Reset
act(() => {
result.current.reset();
});
// Verify localStorage removeItem was called
expect(localStorageMock.removeItem).toHaveBeenCalledWith("org-creation-onboarding");
});
});
});
describe("useOnboarding Hook", () => {
beforeEach(() => {
mockUseSession.mockReturnValue(createTestSession());
});
describe("Authentication Handling", () => {
it("should redirect to login when session is unauthenticated", async () => {
mockUseSession.mockReturnValue(createTestSession({ status: "unauthenticated" }));
mockUsePathname.mockReturnValue("/settings/organizations/new");
mockUseSearchParams.mockReturnValue(createTestSearchParams());
renderHook(() => useOnboarding(), { wrapper: createWrapper() });
await waitFor(() => {
expect(mockRouterPush).toHaveBeenCalledWith(
`/auth/login?callbackUrl=${WEBAPP_URL}/settings/organizations/new`
);
});
mockRouterPush.mockClear();
expect(mockRouterPush).not.toHaveBeenCalled();
});
it("should not redirect when session is authenticated", () => {
expect(mockRouterPush).not.toHaveBeenCalled();
mockUseSession.mockReturnValue(createTestSession({ status: "authenticated" }));
renderHook(() => useOnboarding(), { wrapper: createWrapper() });
expect(mockRouterPush).not.toHaveBeenCalled();
});
});
describe("Organization Onboarding State Sync", () => {
it("should return organization onboarding data when available", () => {
const organizationOnboarding = createTestOrganizationOnboarding({
name: "Test Org",
slug: "test-org",
});
(
trpc.viewer.organizations.getOrganizationOnboarding.useQuery as ReturnType<typeof vi.fn>
).mockReturnValue({
data: organizationOnboarding,
isPending: false,
isLoading: false,
error: null,
refetch: vi.fn(),
});
const { result } = renderHook(() => useOnboarding(), { wrapper: createWrapper() });
expect(result.current.dbOnboarding).toEqual(organizationOnboarding);
expect(result.current.isLoadingOrgOnboarding).toBe(false);
});
it("should set onboardingData from DB to zustand if not set already", async () => {
const organizationOnboarding = createTestOrganizationOnboarding({
id: "sync-test-id",
name: "Synced Organization",
slug: "synced-org",
bio: "Synced bio content",
logo: "synced-logo.png",
billingPeriod: "ANNUALLY",
pricePerSeat: 25,
seats: 10,
orgOwnerEmail: "owner@synced.com",
});
(
trpc.viewer.organizations.getOrganizationOnboarding.useQuery as ReturnType<typeof vi.fn>
).mockReturnValue({
data: organizationOnboarding,
isPending: false,
isLoading: false,
error: null,
refetch: vi.fn(),
});
renderHook(() => useOnboarding(), { wrapper: createWrapper() });
const { result: storeResult } = renderHook(() => useOnboardingStore(), { wrapper: createWrapper() });
await waitFor(() => {
expect(storeResult.current.onboardingId).toBe("sync-test-id");
});
expect(storeResult.current.name).toBe("Synced Organization");
expect(storeResult.current.slug).toBe("synced-org");
expect(storeResult.current.bio).toBe("Synced bio content");
expect(storeResult.current.logo).toBe("synced-logo.png");
expect(storeResult.current.billingPeriod).toBe("ANNUALLY");
expect(storeResult.current.pricePerSeat).toBe(25);
expect(storeResult.current.seats).toBe(10);
expect(storeResult.current.orgOwnerEmail).toBe("owner@synced.com");
});
it("should sync onboardingData from DB to zustand if set already", async () => {
mockOrganizationOnboardingFromDB(
createTestOrganizationOnboarding({
id: "onboarding1",
name: "Onboarding 1 Organization",
slug: "onboarding1-org",
bio: "Onboarding 1 bio content",
logo: "onboarding1-logo.png",
billingPeriod: "MONTHLY",
pricePerSeat: 15,
seats: 5,
orgOwnerEmail: "owner@onboarding1.com",
})
);
// Load the store to set the initial state
const { result: storeResultBefore } = renderUseOnboardingStore();
expect(storeResultBefore.current.name).toBe("Onboarding 1 Organization");
expect(storeResultBefore.current.slug).toBe("onboarding1-org");
mockOrganizationOnboardingFromDB(
createTestOrganizationOnboarding({
id: "sync-test-id",
name: "Synced Organization",
slug: "synced-org",
bio: "Synced bio content",
logo: "synced-logo.png",
billingPeriod: "ANNUALLY",
pricePerSeat: 25,
seats: 10,
orgOwnerEmail: "owner@synced.com",
})
);
// Load the store to sync the new data
const { result: storeResultAfter } = renderUseOnboardingStore();
// Verify the data is synced
expect(storeResultAfter.current.name).toBe("Synced Organization");
expect(storeResultAfter.current.slug).toBe("synced-org");
});
});
});
});