Files
cal.diy/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx
T
Keith WilliamsandDevin AI 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

502 lines
15 KiB
TypeScript

import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import type { Mock } from "vitest";
import { vi, beforeEach, afterEach, describe, expect, it } from "vitest";
import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute";
import { TestFormRenderer } from "./TestForm";
vi.mock("framer-motion", async () => {
return {
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
AnimatePresence: ({ children }: any) => <>{children}</>,
};
});
vi.mock("@calcom/app-store/routing-forms/lib/processRoute", () => ({
findMatchingRoute: vi.fn(),
}));
vi.mock("next/navigation", async (importOriginal) => {
const actual = await importOriginal<typeof import("next/navigation")>();
return {
...actual,
useRouter: vi.fn(() => ({
push: vi.fn(() => {
return;
}),
})),
};
});
function mockMatchingRoute(route: any) {
(findMatchingRoute as Mock<typeof findMatchingRoute>).mockReturnValue({
...route,
id: "matching-route-id",
});
}
function mockCustomPageMessageMatchingRoute() {
mockMatchingRoute({
action: {
type: "customPageMessage",
value: "Thank you for submitting!",
},
});
}
function mockEventTypeRedirectUrlMatchingRoute() {
mockMatchingRoute({
action: {
type: "eventTypeRedirectUrl",
value: "john/30min",
eventTypeId: 123,
},
});
}
/**
* fixes the error due to Formbricks
*/
vi.mock("@calcom/web/modules/shell/Shell", () => ({
ShellMain: vi.fn(),
}));
vi.mock("@calcom/lib/hooks/useApp", () => ({
default: vi.fn(),
}));
/**
* Avoids the error due to Formbricks
*/
vi.mock("./FormActions", () => ({
FormAction: vi.fn(),
FormActionsDropdown: vi.fn(),
FormActionsProvider: vi.fn(),
}));
vi.mock(
"@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets",
async (importOriginal) => {
return await importOriginal();
}
);
// Mock the necessary dependencies
vi.mock("@calcom/lib/hooks/useLocale", () => ({
useLocale: vi.fn(() => ({ t: (key: string) => key })),
}));
vi.mock("@calcom/features/ee/organizations/context/provider", () => ({
useOrgBranding: vi.fn(() => null),
}));
let findTeamMembersMatchingAttributeLogicResponse: {
result: { users: { email: string }[] } | null;
checkedFallback: boolean;
mainWarnings?: string[] | null;
fallbackWarnings?: string[] | null;
} = {
result: null,
checkedFallback: false,
mainWarnings: null,
fallbackWarnings: null,
};
function resetFindTeamMembersMatchingAttributeLogicResponse() {
findTeamMembersMatchingAttributeLogicResponse = {
result: null,
checkedFallback: false,
mainWarnings: null,
fallbackWarnings: null,
};
}
function mockFindTeamMembersMatchingAttributeLogicResponse(
response: typeof findTeamMembersMatchingAttributeLogicResponse
) {
findTeamMembersMatchingAttributeLogicResponse = response;
}
vi.mock("@calcom/trpc/react", () => ({
trpc: {
viewer: {
routingForms: {
findTeamMembersMatchingAttributeLogicOfRoute: {
useMutation: vi.fn(({ onSuccess }) => {
return {
mutate: vi.fn(() => {
onSuccess(findTeamMembersMatchingAttributeLogicResponse);
}),
};
}),
},
},
},
},
}));
const mockSubTeamForm = {
id: "routing-form-id",
teamId: "test-team-id",
name: "Test Form",
description: "Test form description",
fields: [
{
id: "name",
identifier: "name",
type: "text",
label: "Name",
required: true,
},
],
routes: [
{
id: "non-matching-route-id",
isFallback: false,
action: {
type: "customPageMessage",
value: "Not matching",
},
},
{
id: "matching-route-id",
isFallback: false,
action: {
type: "customPageMessage",
value: "Thank you for submitting!",
},
},
{
id: "fallback-route",
isFallback: true,
action: {
type: "customPageMessage",
value: "Thank you for submitting!",
},
},
],
team: {
parentId: "org-1",
},
} as any;
const mockRegularTeamForm = {
...mockSubTeamForm,
team: {
parentId: null,
},
} as any;
describe("TestFormDialog", () => {
beforeEach(() => {
resetFindTeamMembersMatchingAttributeLogicResponse();
vi.clearAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
// Flush any pending timers (like Radix FocusScope setTimeout) before cleanup
// to prevent them from firing after jsdom teardown
vi.runOnlyPendingTimers();
vi.useRealTimers();
cleanup();
});
it("renders the dialog when open", () => {
render(
<TestFormRenderer
isMobile={true}
testForm={mockSubTeamForm}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
expect(screen.getByText("test_routing_form")).toBeInTheDocument();
expect(screen.getByText("test_preview_description")).toBeInTheDocument();
});
it("doesn't render the dialog when closed", () => {
render(
<TestFormRenderer
isMobile={true}
testForm={mockSubTeamForm}
isTestPreviewOpen={false}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
expect(screen.queryByText("test_routing_form")).not.toBeInTheDocument();
});
it("renders form fields", () => {
render(
<TestFormRenderer
isMobile={true}
testForm={mockSubTeamForm}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
expect(screen.getByTestId("form-field-name")).toBeInTheDocument();
});
describe("Sub-Team Form", () => {
const form = mockSubTeamForm;
it("submits the form and shows test results for Custom Page", async () => {
mockCustomPageMessageMatchingRoute();
render(
<TestFormRenderer
isMobile={true}
testForm={form}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
expect(screen.getByTestId("test-routing-result")).toHaveTextContent("Thank you for submitting!");
});
it("submits the form and shows test results for Event Type", async () => {
mockEventTypeRedirectUrlMatchingRoute();
render(
<TestFormRenderer
isMobile={true}
testForm={form}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
expect(screen.getByTestId("test-routing-result")).toHaveTextContent("john/30min");
expect(screen.getByTestId("attribute-logic-matched")).toHaveTextContent("Yes");
expect(screen.getByTestId("attribute-logic-fallback-matched")).toHaveTextContent("Not needed");
// Skip the matching members check as it's giving issues
});
it("suggests to add fallback when matching members is empty and fallback is not checked", async () => {
mockEventTypeRedirectUrlMatchingRoute();
mockFindTeamMembersMatchingAttributeLogicResponse({
result: {
users: [],
},
checkedFallback: false,
});
render(
<TestFormRenderer
isMobile={true}
testForm={form}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
expect(screen.getByTestId("test-routing-result")).toHaveTextContent("john/30min");
expect(screen.getByTestId("attribute-logic-matched")).toHaveTextContent("Yes");
expect(screen.getByTestId("attribute-logic-fallback-matched")).toHaveTextContent("Not needed");
// Skip the matching members check as it's giving issues
});
it("shows warnings when there are warnings", async () => {
mockEventTypeRedirectUrlMatchingRoute();
mockFindTeamMembersMatchingAttributeLogicResponse({
result: null,
checkedFallback: false,
mainWarnings: ["Main-Error-1", "Main-Error-2"],
fallbackWarnings: ["Fallback-Error-1", "Fallback-Error-2"],
});
render(
<TestFormRenderer
isMobile={true}
testForm={form}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
// Get all alerts without checking their specific count
const alerts = screen.getAllByTestId("alert");
// Verify that at least the main and fallback warnings are present
expect(alerts.some((alert) => alert.textContent?.includes("Main-Error-1"))).toBe(true);
expect(alerts.some((alert) => alert.textContent?.includes("Main-Error-2"))).toBe(true);
expect(alerts.some((alert) => alert.textContent?.includes("Fallback-Error-1"))).toBe(true);
expect(alerts.some((alert) => alert.textContent?.includes("Fallback-Error-2"))).toBe(true);
});
it("should not show warnings when there are no warnings", async () => {
mockEventTypeRedirectUrlMatchingRoute();
mockFindTeamMembersMatchingAttributeLogicResponse({
result: null,
checkedFallback: false,
mainWarnings: null,
fallbackWarnings: null,
});
render(
<TestFormRenderer
isMobile={true}
testForm={form}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
screen.logTestingPlaygroundURL();
const alerts = screen.queryAllByTestId("alert");
expect(alerts).toHaveLength(0);
});
it("should show No in main and fallback matched", async () => {
mockEventTypeRedirectUrlMatchingRoute();
mockFindTeamMembersMatchingAttributeLogicResponse({
result: {
users: [],
},
checkedFallback: true,
mainWarnings: null,
fallbackWarnings: null,
});
render(
<TestFormRenderer
isMobile={true}
testForm={form}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
expect(screen.getByTestId("attribute-logic-matched")).toHaveTextContent("No");
expect(screen.getByTestId("attribute-logic-fallback-matched")).toHaveTextContent("Yes");
// Skip the matching members check as it's giving issues
});
});
describe("Regular Team Form", () => {
const form = mockRegularTeamForm;
it("submits the form and shows test results for Custom Page", async () => {
mockCustomPageMessageMatchingRoute();
render(
<TestFormRenderer
isMobile={true}
testForm={mockRegularTeamForm}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
expect(screen.getByTestId("test-routing-result")).toHaveTextContent("Thank you for submitting!");
});
it("submits the form and shows test results for Event Type", async () => {
mockEventTypeRedirectUrlMatchingRoute();
render(
<TestFormRenderer
isMobile={true}
testForm={form}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
expect(screen.getByTestId("test-routing-result")).toHaveTextContent("john/30min");
// When we support showing matching route we can add this back
// expect(screen.getByTestId("chosen-route")).toHaveTextContent("Route 2");
});
it("should substitute variables in event redirect URL", async () => {
// Mock a route with variables using the name field that already exists
mockMatchingRoute({
action: {
type: "eventTypeRedirectUrl",
value: "/team/{name}/meeting",
},
});
render(
<TestFormRenderer
isMobile={true}
testForm={mockRegularTeamForm}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
// Fill in the name field
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "Sales Team" } });
fireEvent.click(screen.getByText("submit"));
// Verify the URL shows the substituted value, not the variable
expect(screen.getByTestId("test-routing-result")).toHaveTextContent("/team/Sales%20Team/meeting");
expect(screen.getByTestId("test-routing-result")).not.toHaveTextContent("/team/sales-team/meeting");
expect(screen.getByTestId("test-routing-result")).not.toHaveTextContent("{name}");
});
it("should NOT substitute variables in external redirect URL", async () => {
// Mock a route with variables for external redirect
mockMatchingRoute({
action: {
type: "externalRedirectUrl",
value: "https://example.com/user/{name}",
},
});
render(
<TestFormRenderer
isMobile={true}
testForm={mockRegularTeamForm}
isTestPreviewOpen={true}
setIsTestPreviewOpen={() => {
return;
}}
/>
);
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
fireEvent.click(screen.getByText("submit"));
// Verify the URL shows the variable as-is, without substitution
expect(screen.getByTestId("test-routing-result")).toHaveTextContent("https://example.com/user/{name}");
expect(screen.getByTestId("test-routing-result")).not.toHaveTextContent("john-doe");
});
});
});