* 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>
502 lines
15 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|