mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-04 03:16:15 -05:00
437 lines
13 KiB
TypeScript
437 lines
13 KiB
TypeScript
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
|
import { getTeamsByOrganizationIdAction } from "@/modules/projects/settings/actions";
|
|
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { toast } from "react-hot-toast";
|
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
import { CreateProjectModal } from "./index";
|
|
|
|
// Mock dependencies
|
|
vi.mock("@/app/(app)/environments/[environmentId]/actions", () => ({
|
|
createProjectAction: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/modules/projects/settings/actions", () => ({
|
|
getTeamsByOrganizationIdAction: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/utils/helper", () => ({
|
|
getFormattedErrorMessage: vi.fn(),
|
|
}));
|
|
|
|
const mockPush = vi.fn();
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({
|
|
push: mockPush,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("react-hot-toast", () => ({
|
|
toast: {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("@tolgee/react", () => ({
|
|
useTranslate: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}));
|
|
|
|
// Mock UI components
|
|
vi.mock("@/modules/ui/components/dialog", () => ({
|
|
Dialog: ({ open, onOpenChange, children }: any) =>
|
|
open ? (
|
|
<div data-testid="dialog" onClick={() => onOpenChange(false)}>
|
|
{children}
|
|
</div>
|
|
) : null,
|
|
DialogContent: ({ children }: any) => <div data-testid="dialog-content">{children}</div>,
|
|
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
|
|
DialogTitle: ({ children }: any) => <h2 data-testid="dialog-title">{children}</h2>,
|
|
DialogDescription: ({ children }: any) => <p data-testid="dialog-description">{children}</p>,
|
|
DialogBody: ({ children }: any) => <div data-testid="dialog-body">{children}</div>,
|
|
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
|
|
}));
|
|
|
|
// Create a mutable form mock that can be modified per test
|
|
let currentFormMock: any;
|
|
|
|
const createFormMock = (options: { shouldCallOnSubmit?: boolean; isSubmitting?: boolean } = {}) => ({
|
|
handleSubmit: vi.fn((onSubmit) => (e: any) => {
|
|
e.preventDefault();
|
|
if (options.shouldCallOnSubmit !== false) {
|
|
onSubmit({ name: "Test Project", teamIds: [] });
|
|
}
|
|
}),
|
|
formState: { isSubmitting: options.isSubmitting || false },
|
|
reset: vi.fn(),
|
|
watch: vi.fn(),
|
|
getValues: vi.fn(() => ({ teamIds: [] })),
|
|
setValue: vi.fn(),
|
|
control: {},
|
|
});
|
|
|
|
vi.mock("react-hook-form", () => ({
|
|
useForm: () => currentFormMock,
|
|
}));
|
|
|
|
vi.mock("@/modules/ui/components/form", () => ({
|
|
FormProvider: ({ children }: any) => <div data-testid="form-provider">{children}</div>,
|
|
FormField: ({ render, name }: any) => {
|
|
const field = {
|
|
value: name === "name" ? "Test Project" : [],
|
|
onChange: vi.fn(),
|
|
};
|
|
return render({ field, fieldState: {} });
|
|
},
|
|
FormItem: ({ children }: any) => <div data-testid="form-item">{children}</div>,
|
|
FormLabel: ({ children }: any) => <label data-testid="form-label">{children}</label>,
|
|
FormControl: ({ children }: any) => <div data-testid="form-control">{children}</div>,
|
|
FormError: ({ children }: any) => <span data-testid="form-error">{children}</span>,
|
|
}));
|
|
|
|
vi.mock("@/modules/ui/components/input", () => ({
|
|
Input: (props: any) => <input data-testid="input" {...props} />,
|
|
}));
|
|
|
|
vi.mock("@/modules/ui/components/multi-select", () => ({
|
|
MultiSelect: ({ value, options, onChange, placeholder }: any) => (
|
|
<select
|
|
data-testid="multi-select"
|
|
data-placeholder={placeholder}
|
|
multiple
|
|
value={value}
|
|
onChange={(e) => onChange(Array.from(e.target.selectedOptions, (option) => option.value))}>
|
|
{options.map((option: any) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/modules/ui/components/button", () => ({
|
|
Button: ({ children, onClick, type, loading, variant, ...props }: any) => (
|
|
<button
|
|
data-testid={`button-${type || "button"}`}
|
|
data-variant={variant}
|
|
data-loading={loading}
|
|
onClick={onClick}
|
|
type={type}
|
|
{...props}>
|
|
{children}
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
describe("CreateProjectModal", () => {
|
|
const mockOrganizationTeams = [
|
|
{ id: "team-1", name: "Development Team" },
|
|
{ id: "team-2", name: "Marketing Team" },
|
|
];
|
|
|
|
const mockProject = {
|
|
id: "project-123",
|
|
name: "Test Project",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
organizationId: "org-123",
|
|
recontactDays: 7,
|
|
inAppSurveyBranding: true,
|
|
linkSurveyBranding: true,
|
|
config: { channel: "website" as const, industry: "saas" as const },
|
|
placement: "bottomRight" as const,
|
|
clickOutsideClose: true,
|
|
darkOverlay: false,
|
|
brandColor: "#000000",
|
|
highlightBorderColor: "#000000",
|
|
styling: { allowStyleOverwrite: true },
|
|
logo: null,
|
|
languages: [],
|
|
environments: [
|
|
{
|
|
id: "env-123",
|
|
type: "production" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
projectId: "project-123",
|
|
appSetupCompleted: false,
|
|
},
|
|
],
|
|
};
|
|
|
|
const defaultProps = {
|
|
open: true,
|
|
setOpen: vi.fn(),
|
|
organizationId: "org-123",
|
|
isAccessControlAllowed: true,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// Reset all mocks
|
|
vi.clearAllMocks();
|
|
|
|
// Reset form mock to default
|
|
currentFormMock = createFormMock();
|
|
|
|
// Mock successful teams fetch
|
|
vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({
|
|
data: mockOrganizationTeams,
|
|
} as any);
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
test("renders modal when open is true", async () => {
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
expect(screen.getByTestId("dialog")).toBeInTheDocument();
|
|
expect(screen.getByTestId("dialog-title")).toHaveTextContent("common.create_project");
|
|
expect(screen.getByTestId("dialog-description")).toHaveTextContent("common.project_creation_description");
|
|
expect(screen.getByTestId("input")).toBeInTheDocument();
|
|
});
|
|
|
|
test("does not render when open is false", () => {
|
|
render(<CreateProjectModal {...defaultProps} open={false} />);
|
|
|
|
expect(screen.queryByTestId("dialog")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("fetches organization teams on mount", async () => {
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(getTeamsByOrganizationIdAction).toHaveBeenCalledWith({
|
|
organizationId: "org-123",
|
|
});
|
|
});
|
|
});
|
|
|
|
test("shows team selection when isAccessControlAllowed is true and teams exist", async () => {
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("multi-select")).toBeInTheDocument();
|
|
});
|
|
|
|
const multiSelect = screen.getByTestId("multi-select");
|
|
expect(multiSelect).toHaveAttribute("data-placeholder", "common.select_teams");
|
|
|
|
const options = screen.getAllByRole("option");
|
|
expect(options).toHaveLength(2);
|
|
expect(options[0]).toHaveTextContent("Development Team");
|
|
expect(options[1]).toHaveTextContent("Marketing Team");
|
|
});
|
|
|
|
test("hides team selection when isAccessControlAllowed is false", async () => {
|
|
render(<CreateProjectModal {...defaultProps} isAccessControlAllowed={false} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId("multi-select")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
test("hides team selection when no teams exist", async () => {
|
|
vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({
|
|
data: [],
|
|
} as any);
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId("multi-select")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
test("handles teams fetch error", async () => {
|
|
const errorMessage = "Failed to fetch teams";
|
|
vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({
|
|
serverError: "Failed to fetch teams",
|
|
} as any);
|
|
vi.mocked(getFormattedErrorMessage).mockReturnValue(errorMessage);
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
|
});
|
|
});
|
|
|
|
test("submits form with project name only when no teams selected", async () => {
|
|
const user = userEvent.setup();
|
|
|
|
vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any);
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("input")).toBeInTheDocument();
|
|
});
|
|
|
|
const submitButton = screen.getByTestId("button-submit");
|
|
await user.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(createProjectAction).toHaveBeenCalledWith({
|
|
organizationId: "org-123",
|
|
data: {
|
|
name: "Test Project",
|
|
teamIds: [],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
test("submits form with selected teams", async () => {
|
|
const user = userEvent.setup();
|
|
|
|
// Update form mock to return teams
|
|
currentFormMock = createFormMock();
|
|
currentFormMock.handleSubmit = vi.fn((onSubmit) => (e: any) => {
|
|
e.preventDefault();
|
|
onSubmit({ name: "Test Project", teamIds: ["team-1", "team-2"] });
|
|
});
|
|
|
|
vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any);
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("input")).toBeInTheDocument();
|
|
expect(screen.getByTestId("multi-select")).toBeInTheDocument();
|
|
});
|
|
|
|
const submitButton = screen.getByTestId("button-submit");
|
|
await user.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(createProjectAction).toHaveBeenCalledWith({
|
|
organizationId: "org-123",
|
|
data: {
|
|
name: "Test Project",
|
|
teamIds: ["team-1", "team-2"],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
test("shows success message and redirects on successful creation", async () => {
|
|
const user = userEvent.setup();
|
|
|
|
vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any);
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("input")).toBeInTheDocument();
|
|
});
|
|
|
|
const submitButton = screen.getByTestId("button-submit");
|
|
await user.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(createProjectAction).toHaveBeenCalled();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(toast.success).toHaveBeenCalledWith("Project created successfully");
|
|
expect(defaultProps.setOpen).toHaveBeenCalledWith(false);
|
|
expect(mockPush).toHaveBeenCalledWith("/environments/env-123/surveys");
|
|
});
|
|
});
|
|
|
|
test("shows error message on creation failure", async () => {
|
|
const user = userEvent.setup();
|
|
const errorMessage = "Project creation failed";
|
|
|
|
vi.mocked(createProjectAction).mockResolvedValue({
|
|
serverError: "Creation failed",
|
|
} as any);
|
|
vi.mocked(getFormattedErrorMessage).mockReturnValue(errorMessage);
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("input")).toBeInTheDocument();
|
|
});
|
|
|
|
const submitButton = screen.getByTestId("button-submit");
|
|
await user.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
|
});
|
|
});
|
|
|
|
test("closes modal and resets form when cancel button clicked", async () => {
|
|
const user = userEvent.setup();
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("input")).toBeInTheDocument();
|
|
});
|
|
|
|
const cancelButton = screen.getByTestId("button-button");
|
|
await user.click(cancelButton);
|
|
|
|
expect(defaultProps.setOpen).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
test("shows loading state during form submission", async () => {
|
|
// Mock loading state
|
|
currentFormMock = createFormMock({ isSubmitting: true });
|
|
|
|
vi.mocked(createProjectAction).mockImplementation(
|
|
() => new Promise((resolve) => setTimeout(() => resolve({ data: mockProject }), 100))
|
|
);
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("input")).toBeInTheDocument();
|
|
});
|
|
|
|
// Check that loading state is shown
|
|
const submitButton = screen.getByTestId("button-submit");
|
|
expect(submitButton).toHaveAttribute("data-loading", "true");
|
|
});
|
|
|
|
test("handles form validation errors", async () => {
|
|
const user = userEvent.setup();
|
|
|
|
// Mock form with validation error - don't call onSubmit
|
|
currentFormMock = createFormMock({ shouldCallOnSubmit: false });
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("input")).toBeInTheDocument();
|
|
});
|
|
|
|
const submitButton = screen.getByTestId("button-submit");
|
|
await user.click(submitButton);
|
|
|
|
// Form should not submit if validation fails
|
|
expect(createProjectAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("closes modal when dialog background is clicked", async () => {
|
|
const user = userEvent.setup();
|
|
|
|
render(<CreateProjectModal {...defaultProps} />);
|
|
|
|
const dialog = screen.getByTestId("dialog");
|
|
await user.click(dialog);
|
|
|
|
expect(defaultProps.setOpen).toHaveBeenCalledWith(false);
|
|
});
|
|
});
|