Files
calendar/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx
T
Keith WilliamsGitHubkeith@cal.com <keithwillcode@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
64dcf5d3f6 fix: add cleanup and mock embed-iframe to prevent test teardown leak (#26377)
* fix: add cleanup and mock embed-iframe to prevent test teardown leak

The CancelBooking.cancellationFee.test.tsx was causing an unhandled jsdom
exception during test teardown due to the @calcom/embed-core/embed-iframe
module scheduling timers that would fire after the jsdom environment was
destroyed.

Changes:
- Mock @calcom/embed-core/embed-iframe to prevent sdkActionManager from
  scheduling timers during tests
- Add afterEach cleanup to ensure React Testing Library properly cleans up
  between tests
- Remove unused React import

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add afterAll cleanup to restore scrollIntoView and unmock embed-iframe

Add proper cleanup in afterAll to:
- Restore Element.prototype.scrollIntoView to its original value
- Call vi.unmock for embed-iframe to avoid polluting other tests in the same worker

This prevents cross-test pollution that was causing flaky 'Closing rpc while fetch was pending' errors in other test files running in the same Vitest worker.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add cleanup to TestFormDialog and defer imports in editLocation.handler tests

- TestFormDialog.test.tsx: Add fake timers and flush pending timers before cleanup
  to prevent Radix FocusScope setTimeout from firing after jsdom teardown
- editLocation.handler.test.ts: Remove top-level imports to prevent watchlist
  module loading during test collection (tests are already skipped)

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: defer imports in confirm.handler.test.ts to prevent Salesforce GraphQL module loading

Tests are already skipped, so imports are not needed during collection phase.
This prevents 'Closing rpc while fetch was pending' errors from Salesforce GraphQL module imports.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-02 00:10:55 +00:00

278 lines
7.9 KiB
TypeScript

import { render, screen, cleanup } from "@testing-library/react";
import { describe, expect, it, vi, beforeAll, afterAll, afterEach } from "vitest";
import * as shouldChargeModule from "@calcom/features/bookings/lib/payment/shouldChargeNoShowCancellationFee";
import CancelBooking from "../CancelBooking";
// Mock the embed-iframe module to prevent it from scheduling timers/RAF that can cause
// teardown issues when jsdom environment is destroyed
vi.mock("@calcom/embed-core/embed-iframe", () => ({
sdkActionManager: null,
}));
// Store original scrollIntoView to restore later
const originalScrollIntoView = Element.prototype.scrollIntoView;
beforeAll(() => {
// jsdom doesn't implement scrollIntoView, so we need to mock it
Element.prototype.scrollIntoView = vi.fn();
});
afterAll(() => {
// Restore scrollIntoView to avoid polluting other tests in the same worker
if (originalScrollIntoView) {
Element.prototype.scrollIntoView = originalScrollIntoView;
} else {
// If it was originally undefined, delete it
delete (Element.prototype as { scrollIntoView?: unknown }).scrollIntoView;
}
// Clean up module mocks to avoid polluting other tests
vi.unmock("@calcom/embed-core/embed-iframe");
});
afterEach(() => {
cleanup();
});
vi.mock("@calcom/trpc/react", () => ({
trpc: {
viewer: {
bookings: {
requestReschedule: {
useMutation: () => ({
mutate: vi.fn(),
isLoading: false,
}),
},
},
},
},
}));
vi.mock("next-i18next", () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => {
if (key === "cancellation_fee_warning_cancel") {
const opts = options as {
time?: string;
unit?: string;
amount?: number;
formatParams?: { amount?: { currency?: string } };
};
return `Cancelling within ${opts?.time} ${opts?.unit} will result in a ${opts?.amount} ${opts?.formatParams?.amount?.currency} cancellation fee being charged to your card.`;
}
return key;
},
}),
}));
vi.mock("@calcom/lib/hooks/useLocale", () => ({
useLocale: () => ({
t: (key: string, options?: Record<string, unknown>) => {
if (key === "cancel_booking_acknowledge_no_show_fee") {
const opts = options as {
timeValue?: number;
timeUnit?: string;
amount?: number;
formatParams?: { amount?: { currency?: string } };
};
return `I acknowledge that cancelling within ${opts?.timeValue} ${opts?.timeUnit} will result in a ${opts?.amount} ${opts?.formatParams?.amount?.currency} cancellation fee being charged to my card.`;
}
return key;
},
}),
}));
vi.mock("@calcom/lib/hooks/useRefreshData", () => ({
useRefreshData: () => vi.fn(),
}));
vi.mock("next/router", () => ({
useRouter: () => ({
push: vi.fn(),
query: {},
}),
}));
vi.mock("@calcom/features/bookings/lib/payment/shouldChargeNoShowCancellationFee", () => ({
shouldChargeNoShowCancellationFee: vi.fn(),
}));
const mockBookingWithCancellationFee = {
uid: "test-booking-uid",
title: "Test Meeting",
id: 123,
startTime: new Date(Date.now() + 30 * 60 * 1000),
payment: {
amount: 1000,
currency: "usd",
appId: "stripe",
},
};
const mockEventTypeMetadataWithFee = {
apps: {
stripe: {
autoChargeNoShowFeeIfCancelled: true,
autoChargeNoShowFeeTimeValue: 1,
autoChargeNoShowFeeTimeUnit: "hours" as const,
paymentOption: "HOLD" as const,
},
},
};
const mockEventTypeMetadataWithoutFee = {
apps: {
stripe: {
autoChargeNoShowFeeIfCancelled: true,
autoChargeNoShowFeeTimeValue: 1,
autoChargeNoShowFeeTimeUnit: "hours" as const,
paymentOption: "HOLD" as const,
},
},
};
const mockProps = {
recurringEvent: null,
setIsCancellationMode: vi.fn(),
theme: "light",
allRemainingBookings: false,
seatReferenceUid: undefined,
currentUserEmail: "test@example.com",
bookingCancelledEventProps: {
booking: {},
organizer: {
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
},
eventType: {},
},
internalNotePresets: [],
renderContext: "booking-single-view" as const,
};
const mockBookingWithoutCancellationFee = {
uid: "test-booking-uid-2",
title: "Test Meeting 2",
id: 124,
startTime: new Date(Date.now() + 2 * 60 * 60 * 1000),
payment: {
amount: 1000,
currency: "usd",
appId: "stripe",
},
};
describe("CancelBooking Cancellation Fee Warning", () => {
it("should show cancellation fee warning when booking is within time threshold", () => {
vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(true);
render(
<CancelBooking
booking={mockBookingWithCancellationFee}
profile={{ name: "Test User", slug: "test-user" }}
team={null}
isHost={false}
eventTypeMetadata={mockEventTypeMetadataWithFee}
{...mockProps}
/>
);
expect(
screen.getByText(/I acknowledge that cancelling within 1 hours will result in a/)
).toBeInTheDocument();
});
it("should not show cancellation fee warning when booking is outside time threshold", () => {
vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(false);
render(
<CancelBooking
booking={mockBookingWithoutCancellationFee}
profile={{ name: "Test User", slug: "test-user" }}
team={null}
isHost={false}
eventTypeMetadata={mockEventTypeMetadataWithoutFee}
{...mockProps}
/>
);
expect(screen.queryByText(/I acknowledge that cancelling within/)).not.toBeInTheDocument();
});
it("should not show cancellation fee warning when user is host", () => {
vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(false);
render(
<CancelBooking
booking={mockBookingWithCancellationFee}
profile={{ name: "Test User", slug: "test-user" }}
team={null}
isHost={true}
eventTypeMetadata={mockEventTypeMetadataWithFee}
{...mockProps}
/>
);
expect(screen.queryByText(/I acknowledge that cancelling within/)).not.toBeInTheDocument();
});
it("should not show cancellation fee warning when cancellation fee is disabled", () => {
const eventTypeMetadataWithDisabledFee = {
apps: {
stripe: {
autoChargeNoShowFeeIfCancelled: false,
autoChargeNoShowFeeTimeValue: 1,
autoChargeNoShowFeeTimeUnit: "hours" as const,
paymentOption: "HOLD" as const,
},
},
};
vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(false);
render(
<CancelBooking
booking={mockBookingWithCancellationFee}
profile={{ name: "Test User", slug: "test-user" }}
team={null}
isHost={false}
eventTypeMetadata={eventTypeMetadataWithDisabledFee}
{...mockProps}
/>
);
expect(screen.queryByText(/I acknowledge that cancelling within/)).not.toBeInTheDocument();
});
it("should not show cancellation fee warning when payment option is not HOLD", () => {
const eventTypeMetadataWithOnBookingPayment = {
apps: {
stripe: {
autoChargeNoShowFeeIfCancelled: true,
autoChargeNoShowFeeTimeValue: 1,
autoChargeNoShowFeeTimeUnit: "hours" as const,
paymentOption: "ON_BOOKING" as const,
},
},
};
vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(false);
render(
<CancelBooking
booking={mockBookingWithCancellationFee}
profile={{ name: "Test User", slug: "test-user" }}
team={null}
isHost={false}
eventTypeMetadata={eventTypeMetadataWithOnBookingPayment}
{...mockProps}
/>
);
expect(screen.queryByText(/I acknowledge that cancelling within/)).not.toBeInTheDocument();
});
});