* fix: change slugification to uri encoding * fix: add conditional joiner symbol between event type redirect url and all other search params * fix: update integration test to expect encoded variable * test: add tests for query parameters joiner logic
489 lines
14 KiB
TypeScript
489 lines
14 KiB
TypeScript
import { render, screen, fireEvent } from "@testing-library/react";
|
|
import type { Mock } from "vitest";
|
|
import { vi } 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/features/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 })),
|
|
}));
|
|
|
|
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();
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|
|
});
|