Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5961240
fix manage test error
sean-brydon Aug 6, 2025
fe7bd68
fix manage test error
sean-brydon Aug 6, 2025
727bc29
Merge branch 'main' into rajiv-demo
Ryukemeister Aug 6, 2025
649a9b3
update useBookerContext
Ryukemeister Aug 7, 2025
de30ff0
wrap store provider around booker
Ryukemeister Aug 7, 2025
a1c8765
replace useStore with useBookerStoreContext
Ryukemeister Aug 7, 2025
40d9495
replace useBookerStore with useBookerStoreContext
Ryukemeister Aug 10, 2025
ab39b6c
add initializer function for booker store provider
Ryukemeister Aug 10, 2025
1adbd90
update props
Ryukemeister Aug 10, 2025
c274667
fixup: pass more props
Ryukemeister Aug 10, 2025
74b0fee
export StoreInitializeType type
Ryukemeister Aug 10, 2025
eaa4cf1
Merge branch 'main' into rajiv-demo
Ryukemeister Aug 10, 2025
ad040ae
replace useBookerStore with useBookerStoreContext
Ryukemeister Aug 10, 2025
5c7963c
fixup: dont wrap BookerStoreProvider around booker directly
Ryukemeister Aug 10, 2025
921c074
wrap BookerStoreProvider around booker web wrapper and fix local stor…
Ryukemeister Aug 10, 2025
5200d69
fix: wrap test components with BookerStoreProvider to fix failing uni…
devin-ai-integration[bot] Aug 11, 2025
2f4484e
Merge branch 'main' into rajiv-demo
Ryukemeister Aug 11, 2025
9faa02e
add changesets
Ryukemeister Aug 11, 2025
4a259b3
Merge branch 'main' into rajiv-demo
Ryukemeister Aug 11, 2025
f716670
Merge branch 'main' into rajiv-demo
hbjORbj Aug 12, 2025
bd1ed1a
Merge branch 'main' into rajiv-demo
Ryukemeister Aug 12, 2025
87d837f
fix: complete migration from useBookerStore to useBookerStoreContext
devin-ai-integration[bot] Aug 12, 2025
12947ca
fix: add missing useBookerStoreContext import in BookerWebWrapper.tsx
devin-ai-integration[bot] Aug 12, 2025
8ccdd02
Revert "fix: add missing useBookerStoreContext import in BookerWebWra…
devin-ai-integration[bot] Aug 13, 2025
4e2ad5a
Revert "fix: complete migration from useBookerStore to useBookerStore…
devin-ai-integration[bot] Aug 13, 2025
e81eeff
fixup: useBookerStoreContext and useInitializeBookerStoreContext for …
Ryukemeister Aug 13, 2025
14d85d9
fixup: failing E2E tests
Ryukemeister Aug 13, 2025
b48345b
fixup: email embed breaking
Ryukemeister Aug 13, 2025
8e6f937
Merge branch 'main' into rajiv-demo
Ryukemeister Aug 13, 2025
9f24f02
fixup: troubleshooter crashing
Ryukemeister Aug 14, 2025
69ff62d
Merge branch 'main' into rajiv-demo
Ryukemeister Aug 14, 2025
d677f5d
replace useBookerstore with useBookerStoreContext
Ryukemeister Aug 14, 2025
a27e129
Merge branch 'main' into rajiv-demo
Ryukemeister Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fresh-rivers-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@calcom/atoms": patch
---

Fixes an issues for the Booker atom wherein when multiple widgets were being placed on the same page, changes made in one widget would also get reflected in the others.

29 changes: 20 additions & 9 deletions packages/features/bookings/Booker/Booker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import dayjs from "@calcom/dayjs";
import PoweredBy from "@calcom/ee/components/PoweredBy";
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
import TurnstileCaptcha from "@calcom/features/auth/Turnstile";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
Expand Down Expand Up @@ -39,7 +40,6 @@ import { NotFound } from "./components/Unavailable";
import { useIsQuickAvailabilityCheckFeatureEnabled } from "./components/hooks/useIsQuickAvailabilityCheckFeatureEnabled";
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
import framerFeatures from "./framer-features";
import { useBookerStore } from "./store";
import type { BookerProps, WrappedBookerProps } from "./types";
import { isBookingDryRun } from "./utils/isBookingDryRun";
import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable";
Expand Down Expand Up @@ -84,9 +84,14 @@ const BookerComponent = ({
}: BookerProps & WrappedBookerProps) => {
const searchParams = useCompatSearchParams();
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
const [bookerState, setBookerState] = useBookerStoreContext(
(state) => [state.state, state.setState],
shallow
);

const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);

const selectedDate = useBookerStore((state) => state.selectedDate);
const {
shouldShowFormInDialog,
hasDarkBackground,
Expand All @@ -99,12 +104,15 @@ const BookerComponent = ({
bookerLayouts,
} = bookerLayout;

const [seatedEventData, setSeatedEventData] = useBookerStore(
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
(state) => [state.seatedEventData, state.setSeatedEventData],
shallow
);
const { selectedTimeslot, setSelectedTimeslot, allSelectedTimeslots } = slots;
const [dayCount, setDayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
const [dayCount, setDayCount] = useBookerStoreContext(
(state) => [state.dayCount, state.setDayCount],
shallow
);

const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
Expand Down Expand Up @@ -183,6 +191,10 @@ const BookerComponent = ({
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
);

const onAvailableTimeSlotSelect = (time: string) => {
setSelectedTimeslot(time);
};

updateEmbedBookerState({ bookerState, slotsQuery: schedule });

useEffect(() => {
Expand Down Expand Up @@ -219,6 +231,7 @@ const BookerComponent = ({
return bookerState === "booking" ? (
<BookEventForm
key={key}
timeslot={selectedTimeslot}
shouldRenderCaptcha={shouldRenderCaptcha}
onCancel={() => {
Comment on lines 233 to 237
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Prop threading sanity-check

Passing timeslot={selectedTimeslot} to BookEventForm and selectedTimeslot to EventMeta requires updated prop types/usages downstream. If not already done, ensure those components accept and use these props.

Run:

Also applies to: 407-407


🏁 Script executed:

#!/bin/bash
rg -n -A2 -B2 'BookEventForm\\(' packages/features/bookings
rg -n -A2 -B2 'interface .*BookEventForm.*props|type .*BookEventForm.*Props' packages/features/bookings
rg -n -A2 -B2 'EventMeta\\(' packages/features/bookings

Length of output: 904


🏁 Script executed:

#!/bin/bash

# Locate BookEventForm usage sites
rg -n 'BookEventForm\(' -A2 -B2 packages/features/bookings

# Inspect the BookEventFormProps definition and check for timeslot prop
rg -n 'type BookEventFormProps' -A10 packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
rg -n 'timeslot' -n packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx

# Locate EventMeta usage sites
rg -n 'EventMeta\(' -A2 -B2 packages/features/bookings

# Inspect the EventMetaProps (if defined) and check for timeslot handling
rg -n 'type EventMetaProps' -A10 packages/features/bookings
rg -n 'timeslot' -n packages/features/bookings

Length of output: 10075


🏁 Script executed:

#!/bin/bash

# Inspect BookEventForm usage context in Booker.tsx
rg -n 'BookEventForm' -A5 -B5 packages/features/bookings/Booker/Booker.tsx

# Search for EventMeta usages and its prop definitions across the repo
rg -n 'EventMeta' -R .
rg -n 'import.*EventMeta' -R packages/features/bookings
rg -n 'type EventMetaProps' -A10 -R packages/features

Length of output: 2807


🏁 Script executed:

#!/bin/bash

# Find where EventMeta is rendered based on bookerState
rg -n 'bookerState ===' -A5 packages/features/bookings/Booker/Booker.tsx

# Locate EventMeta usage and check passed props
rg -n 'EventMeta' -A5 -B5 packages/features/bookings/Booker/Booker.tsx

# Inspect EventMetaProps definition for timeslot
rg -n 'interface EventMetaProps' -A10 packages/features/bookings/Booker/components/EventMeta.tsx

# Verify if timeslot prop is declared or used in EventMeta component
rg -n 'timeslot' -n packages/features/bookings/Booker/components/EventMeta.tsx

Length of output: 4048


Update EventMeta to accept selectedTimeslot

It looks like you’ve correctly threaded the timeslot prop into BookEventForm (its props type already includes timeslot: string | null), but the EventMeta component itself doesn’t define or consume a selectedTimeslot prop. You’ll need to:

• In packages/features/bookings/Booker/components/EventMeta.tsx, add selectedTimeslot?: string | null (or required) to its props definition.
• Destructure and use selectedTimeslot inside EventMeta wherever you need to display or conditionally render based on the currently selected slot.

This will keep the type system happy and ensure EventMeta actually responds to the timeslot state you’re passing in.

🤖 Prompt for AI Agents
In packages/features/bookings/Booker/components/EventMeta.tsx (where the
component props are defined), EventMeta currently lacks a selectedTimeslot prop
even though the parent passes timeslot/selectedTimeslot; add selectedTimeslot?:
string | null (or selectedTimeslot: string | null if required) to the
component's props type, destructure selectedTimeslot from the props in the
component signature, and then reference it where appropriate (render or
conditionally render UI based on the currently selected slot) so the component
consumes the passed timeslot and the TypeScript types align.

setSelectedTimeslot(null);
Expand Down Expand Up @@ -311,9 +324,7 @@ const BookerComponent = ({
return (
<>
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}

{(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />}

<div
className={classNames(
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
Expand Down Expand Up @@ -393,6 +404,7 @@ const BookerComponent = ({
/>
)}
<EventMeta
selectedTimeslot={selectedTimeslot}
classNames={{
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
Expand Down Expand Up @@ -477,6 +489,7 @@ const BookerComponent = ({
ref={timeslotsRef}
{...fadeInLeft}>
<AvailableTimeSlots
onAvailableTimeSlotSelect={onAvailableTimeSlotSelect}
customClassNames={customClassNames?.availableTimeSlotsCustomClassNames}
extraDays={extraDays}
limitHeight={layout === BookerLayouts.MONTH_VIEW}
Expand Down Expand Up @@ -549,7 +562,6 @@ const BookerComponent = ({
</m.span>
)}
</div>

<>
{verifyCode && formEmail ? (
<VerifyCodeDialog
Expand All @@ -568,7 +580,6 @@ const BookerComponent = ({
<></>
)}
</>

<BookFormAsModal
onCancel={() => setSelectedTimeslot(null)}
visible={bookerState === "booking" && shouldShowFormInDialog}>
Expand Down
113 changes: 113 additions & 0 deletions packages/features/bookings/Booker/BookerStoreProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use client";

import { createContext, useContext, useRef, type ReactNode, useEffect } from "react";
import { useStore } from "zustand";
import type { StoreApi } from "zustand";

import { createBookerStore, type BookerStore, type StoreInitializeType } from "./store";

export const BookerStoreContext = createContext<StoreApi<BookerStore> | null>(null);

export interface BookerStoreProviderProps {
children: ReactNode;
}

export const BookerStoreProvider = ({ children }: BookerStoreProviderProps) => {
const storeRef = useRef<StoreApi<BookerStore>>();
if (!storeRef.current) {
storeRef.current = createBookerStore();
}

return <BookerStoreContext.Provider value={storeRef.current}>{children}</BookerStoreContext.Provider>;
};

export const useBookerStoreContext = <T,>(
selector: (store: BookerStore) => T,
equalityFn?: (a: T, b: T) => boolean
): T => {
const bookerStoreContext = useContext(BookerStoreContext);

if (!bookerStoreContext) {
throw new Error("useBookerStoreContext must be used within BookerStoreProvider");
}

return useStore(bookerStoreContext, selector, equalityFn);
};

export const useInitializeBookerStoreContext = ({
username,
eventSlug,
month,
eventId,
rescheduleUid = null,
rescheduledBy = null,
bookingData = null,
verifiedEmail = null,
layout,
isTeamEvent,
durationConfig,
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const bookerStoreContext = useContext(BookerStoreContext);

if (!bookerStoreContext) {
throw new Error("useInitializeBookerStoreContext must be used within BookerStoreProvider");
}

const initializeStore = useStore(bookerStoreContext, (state) => state.initialize);

useEffect(() => {
initializeStore({
username,
eventSlug,
month,
eventId,
rescheduleUid,
rescheduledBy,
bookingData,
layout,
isTeamEvent,
org,
verifiedEmail,
durationConfig,
isInstantMeeting,
timezone,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
});
}, [
initializeStore,
org,
username,
eventSlug,
month,
eventId,
rescheduleUid,
rescheduledBy,
bookingData,
layout,
isTeamEvent,
verifiedEmail,
durationConfig,
isInstantMeeting,
timezone,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
]);
Comment on lines +67 to +112
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider the impact of frequent re-initializations

The useEffect dependency array includes all initialization parameters, which means the store will be re-initialized whenever any prop changes. This could lead to unexpected state resets or performance issues.

Consider whether all these dependencies are necessary. Typically, initialization should only occur when key identifiers change (e.g., username, eventSlug, eventId). Props like timezone, verifiedEmail, or allowUpdatingUrlParams might be better handled through separate update methods rather than full re-initialization.


🏁 Script executed:

#!/bin/bash
# Check how the initialize method handles re-initialization in the store
ast-grep --pattern 'initialize: (data: StoreInitializeType) => {
  $$$
}'

Length of output: 75


I didn’t find an initializeStore implementation with the previous AST query. Let’s locate its definition and see how it handles repeated calls:


🏁 Script executed:

#!/bin/bash
# Search for where initializeStore is defined or imported
rg -n "initializeStore" --glob "*.ts*"

Length of output: 875


Let’s inspect the initialize implementation to see if it fully resets state or merges updates:


🏁 Script executed:

#!/bin/bash
# Show the initialize method in Booker store
sed -n '460,550p' packages/features/bookings/Booker/store.ts

Length of output: 1233


Let’s inspect the actual initialize action in the Booker store to see whether it fully resets state or just merges updates:


🏁 Script executed:

#!/bin/bash
# Locate and display the initialize action implementation
rg -n -C 5 "initialize:" packages/features/bookings/Booker/store.ts

Length of output: 883


Inspecting the full initialize implementation to see whether it resets or merges state:


🏁 Script executed:

#!/bin/bash
# Extract the body of the initialize action
sed -n '295,380p' packages/features/bookings/Booker/store.ts

Length of output: 2812


Align useEffect dependencies with the store’s guard logic

The initialize action in your Booker store only checks a specific subset of fields before early-returning (username, eventSlug, month, eventId, rescheduleUid, bookingUid, bookingData.responses.email, layout, timezone, rescheduledBy, teamMemberEmail, crmOwnerRecordType, crmAppSlug, crmRecordId). However, your useEffect dependency array also includes props—like org, isTeamEvent, durationConfig, isInstantMeeting, verifiedEmail, isPlatform, and allowUpdatingUrlParams—that the guard never compares. This means:

  • Changes to those extra props will trigger initialize calls that immediately return (no state update).
  • Conversely, if you do intend for some of those props to update the store, they’ll be ignored by the guard.

To resolve this mismatch, choose one of:

  • Narrow the dependency array in BookerStoreProvider.tsx to just the keys the guard actually compares.
  • Expand the guard in packages/features/bookings/Booker/store.ts (around lines 295–340) to include any additional props you do want to trigger a re-initialization.
  • Separate out specific update methods (e.g. setTimezone, setAllowUpdatingUrlParams) instead of feeding everything through initialize.

Places to update:

  • packages/features/bookings/Booker/BookerStoreProvider.tsx (lines 67–112) – adjust useEffect deps
  • packages/features/bookings/Booker/store.ts (initialize action at ~lines 295–340) – update guard or parameter handling
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/BookerStoreProvider.tsx around lines 67 to
112, the useEffect dependency array includes props that the store.initialize
guard does not check (org, isTeamEvent, durationConfig, isInstantMeeting,
verifiedEmail, isPlatform, allowUpdatingUrlParams), causing unnecessary
initialize calls or ignored updates; either narrow the dependency array to only
the keys the guard actually compares (username, eventSlug, month, eventId,
rescheduleUid, bookingUid/bookingData.responses.email, layout, timezone,
rescheduledBy, teamMemberEmail, crmOwnerRecordType, crmAppSlug, crmRecordId) or
update the initialize guard in packages/features/bookings/Booker/store.ts
(around lines 295–340) to compare and accept the additional props you want to
trigger re-initialization, or add specific setter actions (e.g., setTimezone,
setAllowUpdatingUrlParams) and call those from the provider instead of
initialize.

};
69 changes: 19 additions & 50 deletions packages/features/bookings/Booker/__tests__/Booker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,17 @@ import "@calcom/features/bookings/Booker/components/__mocks__/Section";
import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
import "@calcom/lib/__mocks__/logger";

import { render, screen } from "@testing-library/react";
import React from "react";
import { vi } from "vitest";

import "@calcom/dayjs/__mocks__";
import "@calcom/features/auth/Turnstile";

import { Booker } from "../Booker";
import { useBookerStore } from "../store";
import type { BookerState } from "../types";
import { render, screen } from "./test-utils";

Comment on lines +20 to 21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add a test that verifies per-instance store isolation (core PR objective)

Good to see the custom render wrapper. However, there’s no test asserting that two Booker instances on the same page don’t share state. Please add one.

Here’s a starting point you can adapt to your test-utils and provider API:

@@
 describe("Booker", () => {
@@
   it("should render DryRunMessage when in dry run mode", () => {
@@
   });
 
+  it("isolates state across multiple Booker instances on the same page", () => {
+    const leftProps = {
+      ...defaultProps,
+      eventSlug: "left-event",
+      slots: {
+        ...defaultProps.slots,
+        setSelectedTimeslot: vi.fn(),
+      },
+    };
+    const rightProps = {
+      ...defaultProps,
+      eventSlug: "right-event",
+      slots: {
+        ...defaultProps.slots,
+        setSelectedTimeslot: vi.fn(),
+      },
+    };
+
+    // Render two instances; ensure each is wrapped with its own store/provider
+    // If your test-utils render() only supports a single mockStore,
+    // render them in two separate calls to simulate two widgets on one page.
+    const left = render(<Booker {...leftProps} />, {
+      mockStore: {
+        state: "booking",
+        selectedDate: "2024-01-01",
+        selectedTimeslot: "2024-01-01T10:00:00Z",
+        tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
+      },
+    });
+    const right = render(<Booker {...rightProps} />, {
+      mockStore: {
+        state: "booking",
+        selectedDate: "2024-01-02",
+        selectedTimeslot: "2024-01-02T10:00:00Z",
+        tentativeSelectedTimeslots: ["2024-01-02T10:00:00Z"],
+      },
+    });
+
+    // Act on the left instance (e.g., cancel)
+    left.getByRole("button", { name: /cancel/i }).click();
+
+    // Assert: left instance props updated, right instance untouched
+    expect((leftProps.slots.setSelectedTimeslot as any)).toHaveBeenCalledWith(null);
+    expect((rightProps.slots.setSelectedTimeslot as any)).not.toHaveBeenCalled();
+  });

If you prefer a single render with both siblings, ensure your test-utils can supply separate store providers per instance; I can help wire that up.

🤖 Prompt for AI Agents
packages/features/bookings/Booker/__tests__/Booker.test.tsx around lines 20-21:
the test file imports the custom render but lacks a test to assert per-instance
store isolation; add a new test that renders two Booker instances with distinct
stores (either by calling your custom render twice with independent
provider/store options or by rendering both siblings inside a wrapper that
supplies separate providers per child), perform an action that mutates the first
instance's state (e.g., select or add a booking), then assert the second
instance's UI/state remains unchanged; ensure the test uses your test-utils APIs
to create isolated stores and only references public UI queries (screen.getBy*,
etc.) to verify independence.

vi.mock("framer-motion", async (importOriginal) => {
const actual = await importOriginal();
const actual = (await importOriginal()) as any;
return {
...actual,
};
Expand Down Expand Up @@ -87,27 +86,6 @@ vi.mock("@calcom/atoms/hooks/useIsPlatform", () => ({
useIsPlatform: () => false,
}));

// Update mockStoreState to include all required state
const mockStoreState = {
state: "booking" as BookerState,
setState: vi.fn(),
selectedDate: "2024-01-01",
seatedEventData: {},
setSeatedEventData: vi.fn(),
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: vi.fn(),
dayCount: 7,
setDayCount: vi.fn(),
setSelectedTimeslot: vi.fn(),
selectedTimeslot: null,
formStep: 0,
setFormStep: vi.fn(),
bookerState: "booking",
setBookerState: vi.fn(),
layout: "default",
setLayout: vi.fn(),
};

// Update defaultProps to include missing required props
const defaultProps = {
username: "testuser",
Expand Down Expand Up @@ -188,32 +166,21 @@ const defaultProps = {
describe("Booker", () => {
beforeEach(() => {
constantsScenarios.set({
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: 100,
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: "100",
POWERED_BY_URL: "https://go.cal.com/booking",
APP_NAME: "Cal.com",
});
vi.clearAllMocks();
});

it("should render null when in loading state", () => {
useBookerStore.setState({
...mockStoreState,
state: "loading",
const { container } = render(<Booker {...defaultProps} />, {
mockStore: { state: "loading" },
});

const { container } = render(<Booker {...defaultProps} />);
expect(container).toBeEmptyDOMElement();
});

it("should render DryRunMessage when in dry run mode", () => {
useBookerStore.setState({
...mockStoreState,
state: "selecting_time",
selectedDate: "2024-01-01",
selectedTimeslot: "2024-01-01T10:00:00Z",
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
});

const propsWithDryRun = {
...defaultProps,
isBookingDryRun: true,
Expand All @@ -226,7 +193,14 @@ describe("Booker", () => {
},
};

render(<Booker {...propsWithDryRun} />);
render(<Booker {...propsWithDryRun} />, {
mockStore: {
state: "selecting_time",
selectedDate: "2024-01-01",
selectedTimeslot: "2024-01-01T10:00:00Z",
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
},
});
expect(screen.getByTestId("dry-run-message")).toBeInTheDocument();
});

Expand All @@ -242,12 +216,10 @@ describe("Booker", () => {
invalidate: mockInvalidate,
},
};
useBookerStore.setState({
...mockStoreState,
state: "booking",
});

render(<Booker {...propsWithInvalidate} />);
render(<Booker {...propsWithInvalidate} />, {
mockStore: { state: "booking" },
});
screen.logTestingPlaygroundURL();
// Trigger form cancel
const cancelButton = screen.getByRole("button", { name: /cancel/i });
Expand All @@ -267,12 +239,9 @@ describe("Booker", () => {
},
};

useBookerStore.setState({
...mockStoreState,
state: "booking",
render(<Booker {...propsWithQuickChecks} />, {
mockStore: { state: "booking" },
});

render(<Booker {...propsWithQuickChecks} />);
const bookEventForm = screen.getByTestId("book-event-form");
await expect(bookEventForm).toHaveAttribute("data-unavailable", "true");
});
Expand Down
Loading
Loading