+
{t("environments.actions.url")}
({
+ Alert: ({ children }: { children: React.ReactNode }) => {children}
,
+ AlertTitle: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ AlertDescription: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
+// Mock the form components
+vi.mock("@/modules/ui/components/form", () => ({
+ FormControl: ({ children }: { children: React.ReactNode }) => {children}
,
+ FormField: ({ render, control }: any) =>
+ render({
+ field: {
+ value: control?._formValues?.noCodeConfig?.type || "",
+ onChange: vi.fn(),
+ },
+ }),
+ FormItem: ({ children }: { children: React.ReactNode }) => {children}
,
+ FormError: () => null,
+}));
+
+// Mock the TabToggle component
+vi.mock("@/modules/ui/components/tab-toggle", () => ({
+ TabToggle: ({ options, onChange, defaultSelected, id, disabled }: any) => (
+
+ {options.map((option: any) => (
+ onChange(option.value)}
+ data-selected={option.value === defaultSelected ? "true" : "false"}>
+ {option.label}
+
+ ))}
+
+ ),
+}));
+
+// Mock the Label component
+vi.mock("@/modules/ui/components/label", () => ({
+ Label: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+}));
+
+// Mock child components
+vi.mock("./components/css-selector", () => ({
+ CssSelector: ({ form, disabled }: any) => (
+
+ CSS Selector
+
+ ),
+}));
+
+vi.mock("./components/inner-html-selector", () => ({
+ InnerHtmlSelector: ({ form, disabled }: any) => (
+
+ Inner HTML Selector
+
+ ),
+}));
+
+vi.mock("./components/page-url-selector", () => ({
+ PageUrlSelector: ({ form, isReadOnly }: any) => (
+
+ Page URL Selector
+
+ ),
+}));
+
+// Mock the tolgee translation
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+// Helper component for the form
+const TestWrapper = ({
+ noCodeConfig = { type: "click" },
+ isReadOnly = false,
+}: {
+ noCodeConfig?: { type: "click" | "pageView" | "exitIntent" | "fiftyPercentScroll" };
+ isReadOnly?: boolean;
+}) => {
+ const form = useForm({
+ defaultValues: {
+ name: "Test Action",
+ description: "Test Description",
+ noCodeConfig,
+ },
+ });
+
+ return ;
+};
+
+describe("NoCodeActionForm", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders the form with click type", () => {
+ render( );
+
+ expect(screen.getByTestId("tab-toggle-userAction")).toBeInTheDocument();
+ expect(screen.getByTestId("tab-option-click")).toHaveAttribute("data-selected", "true");
+ expect(screen.getByTestId("css-selector")).toBeInTheDocument();
+ expect(screen.getByTestId("inner-html-selector")).toBeInTheDocument();
+ expect(screen.getByTestId("page-url-selector")).toBeInTheDocument();
+ });
+
+ test("renders the form with pageView type", () => {
+ render( );
+
+ expect(screen.getByTestId("tab-option-pageView")).toHaveAttribute("data-selected", "true");
+ expect(screen.getByTestId("alert")).toBeInTheDocument();
+ expect(screen.getByTestId("alert-title")).toHaveTextContent("environments.actions.page_view");
+ });
+
+ test("renders the form with exitIntent type", () => {
+ render( );
+
+ expect(screen.getByTestId("tab-option-exitIntent")).toHaveAttribute("data-selected", "true");
+ expect(screen.getByTestId("alert")).toBeInTheDocument();
+ expect(screen.getByTestId("alert-title")).toHaveTextContent("environments.actions.exit_intent");
+ });
+
+ test("renders the form with fiftyPercentScroll type", () => {
+ render( );
+
+ expect(screen.getByTestId("tab-option-fiftyPercentScroll")).toHaveAttribute("data-selected", "true");
+ expect(screen.getByTestId("alert")).toBeInTheDocument();
+ expect(screen.getByTestId("alert-title")).toHaveTextContent("environments.actions.fifty_percent_scroll");
+ });
+
+ test("passes isReadOnly to child components", () => {
+ render( );
+
+ expect(screen.getByTestId("tab-toggle-userAction")).toBeInTheDocument();
+ expect(screen.getByTestId("css-selector")).toHaveAttribute("data-disabled", "true");
+ expect(screen.getByTestId("inner-html-selector")).toHaveAttribute("data-disabled", "true");
+ expect(screen.getByTestId("page-url-selector")).toHaveAttribute("data-readonly", "true");
+ });
+});
diff --git a/apps/web/modules/ui/components/no-mobile-overlay/index.test.tsx b/apps/web/modules/ui/components/no-mobile-overlay/index.test.tsx
new file mode 100644
index 0000000000..667b879660
--- /dev/null
+++ b/apps/web/modules/ui/components/no-mobile-overlay/index.test.tsx
@@ -0,0 +1,38 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { NoMobileOverlay } from "./index";
+
+// Mock the tolgee translation
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) =>
+ key === "common.mobile_overlay_text" ? "Please use desktop to access this section" : key,
+ }),
+}));
+
+describe("NoMobileOverlay", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders overlay with correct text", () => {
+ render( );
+
+ expect(screen.getByText("Please use desktop to access this section")).toBeInTheDocument();
+ });
+
+ test("has proper z-index for overlay", () => {
+ render( );
+
+ const overlay = screen.getByText("Please use desktop to access this section").closest("div.fixed");
+ expect(overlay).toHaveClass("z-[9999]");
+ });
+
+ test("has responsive layout with sm:hidden class", () => {
+ render( );
+
+ const overlay = screen.getByText("Please use desktop to access this section").closest("div.fixed");
+ expect(overlay).toHaveClass("sm:hidden");
+ });
+});
diff --git a/apps/web/modules/ui/components/option-card/index.test.tsx b/apps/web/modules/ui/components/option-card/index.test.tsx
new file mode 100644
index 0000000000..892f4dad7c
--- /dev/null
+++ b/apps/web/modules/ui/components/option-card/index.test.tsx
@@ -0,0 +1,81 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { OptionCard } from "./index";
+
+vi.mock("@/modules/ui/components/loading-spinner", () => ({
+ LoadingSpinner: () => Loading Spinner
,
+}));
+
+describe("OptionCard", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders with small size correctly", () => {
+ render( );
+
+ expect(screen.getByText("Test Title")).toBeInTheDocument();
+ expect(screen.getByText("Test Description")).toBeInTheDocument();
+
+ const card = screen.getByRole("button");
+ expect(card).toHaveClass("p-4", "rounded-lg", "w-60", "shadow-md");
+ });
+
+ test("renders with medium size correctly", () => {
+ render( );
+
+ const card = screen.getByRole("button");
+ expect(card).toHaveClass("p-6", "rounded-xl", "w-80", "shadow-lg");
+ });
+
+ test("renders with large size correctly", () => {
+ render( );
+
+ const card = screen.getByRole("button");
+ expect(card).toHaveClass("p-8", "rounded-2xl", "w-100", "shadow-xl");
+ });
+
+ test("displays loading spinner when loading is true", () => {
+ render( );
+
+ expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
+ });
+
+ test("does not display loading spinner when loading is false", () => {
+ render( );
+
+ expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
+ });
+
+ test("calls onSelect when clicked", async () => {
+ const handleSelect = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click(screen.getByRole("button"));
+ expect(handleSelect).toHaveBeenCalledTimes(1);
+ });
+
+ test("renders with custom cssId", () => {
+ render( );
+
+ const card = screen.getByRole("button");
+ expect(card).toHaveAttribute("id", "custom-id");
+ });
+
+ test("renders children correctly", () => {
+ render(
+
+ Child content
+
+ );
+
+ expect(screen.getByTestId("child-element")).toBeInTheDocument();
+ expect(screen.getByText("Child content")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/options-switch/index.test.tsx b/apps/web/modules/ui/components/options-switch/index.test.tsx
new file mode 100644
index 0000000000..313f8db482
--- /dev/null
+++ b/apps/web/modules/ui/components/options-switch/index.test.tsx
@@ -0,0 +1,92 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { OptionsSwitch } from "./index";
+
+describe("OptionsSwitch", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockOptions = [
+ { value: "option1", label: "Option 1" },
+ { value: "option2", label: "Option 2" },
+ { value: "option3", label: "Option 3", disabled: true },
+ ];
+
+ test("renders all options correctly", () => {
+ render( {}} />);
+
+ expect(screen.getByText("Option 1")).toBeInTheDocument();
+ expect(screen.getByText("Option 2")).toBeInTheDocument();
+ expect(screen.getByText("Option 3")).toBeInTheDocument();
+ });
+
+ test("highlights the current option", () => {
+ render( {}} />);
+
+ // Check that the highlight div exists
+ const highlight = document.querySelector(".absolute.bottom-1.top-1.rounded-md.bg-slate-100");
+ expect(highlight).toBeInTheDocument();
+ });
+
+ test("calls handleOptionChange with option value when clicked", async () => {
+ const handleOptionChange = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click(screen.getByText("Option 2"));
+
+ expect(handleOptionChange).toHaveBeenCalledWith("option2");
+ });
+
+ test("does not call handleOptionChange when disabled option is clicked", async () => {
+ const handleOptionChange = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click(screen.getByText("Option 3"));
+
+ expect(handleOptionChange).not.toHaveBeenCalled();
+ });
+
+ test("renders icons when provided", () => {
+ const optionsWithIcons = [
+ {
+ value: "option1",
+ label: "Option 1",
+ icon: ,
+ },
+ {
+ value: "option2",
+ label: "Option 2",
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByTestId("icon-option1")).toBeInTheDocument();
+ });
+
+ test("updates highlight position when current option changes", () => {
+ const { rerender } = render(
+ {}} />
+ );
+
+ // Re-render with different current option
+ rerender( {}} />);
+
+ // The highlight style should be updated through useEffect
+ // We can verify the component doesn't crash on re-render
+ expect(screen.getByText("Option 2")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/otp-input/index.test.tsx b/apps/web/modules/ui/components/otp-input/index.test.tsx
new file mode 100644
index 0000000000..f087bd058a
--- /dev/null
+++ b/apps/web/modules/ui/components/otp-input/index.test.tsx
@@ -0,0 +1,119 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { OTPInput } from "./index";
+
+describe("OTPInput", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders correct number of input fields", () => {
+ const onChange = vi.fn();
+ render( );
+
+ const inputs = screen.getAllByRole("textbox");
+ expect(inputs).toHaveLength(6);
+ });
+
+ test("displays provided value correctly", () => {
+ const onChange = vi.fn();
+ render( );
+
+ const inputs = screen.getAllByRole("textbox");
+ expect(inputs[0]).toHaveValue("1");
+ expect(inputs[1]).toHaveValue("2");
+ expect(inputs[2]).toHaveValue("3");
+ expect(inputs[3]).toHaveValue("4");
+ expect(inputs[4]).toHaveValue("5");
+ expect(inputs[5]).toHaveValue("6");
+ });
+
+ test("applies custom container class", () => {
+ const onChange = vi.fn();
+ render(
+
+ );
+
+ const container = screen.getAllByRole("textbox")[0].parentElement;
+ expect(container).toHaveClass("test-container-class");
+ });
+
+ test("applies custom input box class", () => {
+ const onChange = vi.fn();
+ render( );
+
+ const inputs = screen.getAllByRole("textbox");
+ inputs.forEach((input) => {
+ expect(input).toHaveClass("test-input-class");
+ });
+ });
+
+ test("disables inputs when disabled prop is true", () => {
+ const onChange = vi.fn();
+ render( );
+
+ const inputs = screen.getAllByRole("textbox");
+ inputs.forEach((input) => {
+ expect(input).toBeDisabled();
+ });
+ });
+
+ test("calls onChange with updated value when input changes", async () => {
+ const user = userEvent.setup();
+ const onChange = vi.fn();
+ render( );
+
+ const inputs = screen.getAllByRole("textbox");
+ await user.click(inputs[3]);
+ await user.keyboard("4");
+
+ expect(onChange).toHaveBeenCalledWith("1234");
+ });
+
+ test("only accepts digit inputs", async () => {
+ const user = userEvent.setup();
+ const onChange = vi.fn();
+ render( );
+
+ const inputs = screen.getAllByRole("textbox");
+ await user.click(inputs[0]);
+ await user.keyboard("a");
+
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ test("moves focus to next input after entering a digit", async () => {
+ const user = userEvent.setup();
+ const onChange = vi.fn();
+ render( );
+
+ const inputs = screen.getAllByRole("textbox");
+ await user.click(inputs[0]);
+ await user.keyboard("1");
+
+ expect(document.activeElement).toBe(inputs[1]);
+ });
+
+ test("navigates inputs with arrow keys", async () => {
+ const user = userEvent.setup();
+ const onChange = vi.fn();
+ render( );
+
+ const inputs = screen.getAllByRole("textbox");
+ await user.click(inputs[1]); // Focus on the 2nd input
+
+ await user.keyboard("{ArrowRight}");
+ expect(document.activeElement).toBe(inputs[2]);
+
+ await user.keyboard("{ArrowLeft}");
+ expect(document.activeElement).toBe(inputs[1]);
+
+ await user.keyboard("{ArrowDown}");
+ expect(document.activeElement).toBe(inputs[2]);
+
+ await user.keyboard("{ArrowUp}");
+ expect(document.activeElement).toBe(inputs[1]);
+ });
+});
diff --git a/apps/web/modules/ui/components/page-content-wrapper/index.test.tsx b/apps/web/modules/ui/components/page-content-wrapper/index.test.tsx
new file mode 100644
index 0000000000..7d931edb9b
--- /dev/null
+++ b/apps/web/modules/ui/components/page-content-wrapper/index.test.tsx
@@ -0,0 +1,48 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { PageContentWrapper } from "./index";
+
+describe("PageContentWrapper", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders children correctly", () => {
+ const { getByText } = render(
+
+ Test Content
+
+ );
+
+ expect(getByText("Test Content")).toBeInTheDocument();
+ });
+
+ test("applies default classes", () => {
+ const { container } = render(
+
+ Test Content
+
+ );
+
+ const wrapper = container.firstChild as HTMLElement;
+ expect(wrapper).toHaveClass("h-full");
+ expect(wrapper).toHaveClass("space-y-6");
+ expect(wrapper).toHaveClass("p-6");
+ });
+
+ test("applies additional className when provided", () => {
+ const { container } = render(
+
+ Test Content
+
+ );
+
+ const wrapper = container.firstChild as HTMLElement;
+ expect(wrapper).toHaveClass("h-full");
+ expect(wrapper).toHaveClass("space-y-6");
+ expect(wrapper).toHaveClass("p-6");
+ expect(wrapper).toHaveClass("bg-gray-100");
+ expect(wrapper).toHaveClass("rounded-lg");
+ });
+});
diff --git a/apps/web/modules/ui/components/page-header/index.test.tsx b/apps/web/modules/ui/components/page-header/index.test.tsx
new file mode 100644
index 0000000000..a5dc2a1248
--- /dev/null
+++ b/apps/web/modules/ui/components/page-header/index.test.tsx
@@ -0,0 +1,58 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { PageHeader } from "./index";
+
+describe("PageHeader", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders page title correctly", () => {
+ render( );
+ expect(screen.getByText("Dashboard")).toBeInTheDocument();
+ expect(screen.getByText("Dashboard")).toHaveClass("text-3xl font-bold text-slate-800 capitalize");
+ });
+
+ test("renders with CTA", () => {
+ render(Add User} />);
+
+ expect(screen.getByText("Users")).toBeInTheDocument();
+ expect(screen.getByTestId("cta-button")).toBeInTheDocument();
+ expect(screen.getByText("Add User")).toBeInTheDocument();
+ });
+
+ test("renders children correctly", () => {
+ render(
+
+ Additional content
+
+ );
+
+ expect(screen.getByText("Settings")).toBeInTheDocument();
+ expect(screen.getByTestId("child-element")).toBeInTheDocument();
+ expect(screen.getByText("Additional content")).toBeInTheDocument();
+ });
+
+ test("renders with both CTA and children", () => {
+ render(
+ New Product}>
+ Product filters
+
+ );
+
+ expect(screen.getByText("Products")).toBeInTheDocument();
+ expect(screen.getByTestId("cta-button")).toBeInTheDocument();
+ expect(screen.getByText("New Product")).toBeInTheDocument();
+ expect(screen.getByTestId("child-element")).toBeInTheDocument();
+ expect(screen.getByText("Product filters")).toBeInTheDocument();
+ });
+
+ test("has border-b class", () => {
+ const { container } = render( );
+ const headerElement = container.firstChild as HTMLElement;
+
+ expect(headerElement).toHaveClass("border-b");
+ expect(headerElement).toHaveClass("border-slate-200");
+ });
+});
diff --git a/apps/web/modules/ui/components/password-input/index.test.tsx b/apps/web/modules/ui/components/password-input/index.test.tsx
new file mode 100644
index 0000000000..253f3607b1
--- /dev/null
+++ b/apps/web/modules/ui/components/password-input/index.test.tsx
@@ -0,0 +1,83 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test } from "vitest";
+import { PasswordInput } from "./index";
+
+describe("PasswordInput", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders password input with type password by default", () => {
+ render( );
+
+ const input = screen.getByPlaceholderText("Enter password");
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveAttribute("type", "password");
+ });
+
+ test("toggles password visibility when eye icon is clicked", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ const input = screen.getByPlaceholderText("Enter password");
+ expect(input).toHaveAttribute("type", "password");
+
+ // Find and click the toggle button (eye icon)
+ const toggleButton = screen.getByRole("button");
+ await user.click(toggleButton);
+
+ // Check if input type changed to text
+ expect(input).toHaveAttribute("type", "text");
+
+ // Click the toggle button again
+ await user.click(toggleButton);
+
+ // Check if input type changed back to password
+ expect(input).toHaveAttribute("type", "password");
+ });
+
+ test("applies custom className to input", () => {
+ render( );
+
+ const input = screen.getByPlaceholderText("Enter password");
+ expect(input).toHaveClass("custom-input-class");
+ });
+
+ test("applies custom containerClassName", () => {
+ render( );
+
+ const container = screen.getByPlaceholderText("Enter password").parentElement;
+ expect(container).toHaveClass("custom-container-class");
+ });
+
+ test("passes through other HTML input attributes", () => {
+ render(
+
+ );
+
+ const input = screen.getByPlaceholderText("Enter password");
+ expect(input).toHaveAttribute("id", "password-field");
+ expect(input).toHaveAttribute("name", "password");
+ expect(input).toHaveAttribute("required");
+ expect(input).toBeDisabled();
+ });
+
+ test("displays EyeIcon when password is hidden", () => {
+ render( );
+
+ const eyeIcon = document.querySelector("svg");
+ expect(eyeIcon).toBeInTheDocument();
+
+ // This is a simple check for the presence of the icon
+ // We can't easily test the exact Lucide icon type in this setup
+ });
+
+ test("toggle button is of type button to prevent form submission", () => {
+ render( );
+
+ const toggleButton = screen.getByRole("button");
+ expect(toggleButton).toHaveAttribute("type", "button");
+ });
+});
diff --git a/apps/web/modules/ui/components/pending-downgrade-banner/index.test.tsx b/apps/web/modules/ui/components/pending-downgrade-banner/index.test.tsx
new file mode 100644
index 0000000000..4512c4dbb6
--- /dev/null
+++ b/apps/web/modules/ui/components/pending-downgrade-banner/index.test.tsx
@@ -0,0 +1,118 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { PendingDowngradeBanner } from "./index";
+
+// Mock the useTranslate hook
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string, params?: any) => {
+ if (key === "common.pending_downgrade") return "Pending Downgrade";
+ if (key === "common.we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable")
+ return "We were unable to verify your license because the license server is unreachable";
+ if (key === "common.you_will_be_downgraded_to_the_community_edition_on_date")
+ return `You will be downgraded to the community edition on ${params?.date}`;
+ if (key === "common.you_are_downgraded_to_the_community_edition")
+ return "You are downgraded to the community edition";
+ if (key === "common.learn_more") return "Learn more";
+ if (key === "common.close") return "Close";
+ return key;
+ },
+ }),
+}));
+
+// Mock next/link
+vi.mock("next/link", () => ({
+ __esModule: true,
+ default: ({ children, href }: { children: React.ReactNode; href: string }) => (
+
+ {children}
+
+ ),
+}));
+
+describe("PendingDowngradeBanner", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders banner when active and isPendingDowngrade are true", () => {
+ const currentDate = new Date();
+ const lastChecked = new Date(currentDate.getTime() - 24 * 60 * 60 * 1000); // One day ago
+
+ render(
+
+ );
+
+ expect(screen.getByText("Pending Downgrade")).toBeInTheDocument();
+ // Check if learn more link is present
+ const learnMoreLink = screen.getByText("Learn more");
+ expect(learnMoreLink).toBeInTheDocument();
+ expect(screen.getByTestId("mock-link")).toHaveAttribute(
+ "href",
+ "/environments/env-123/settings/enterprise"
+ );
+ });
+
+ test("doesn't render when active is false", () => {
+ const currentDate = new Date();
+ const lastChecked = new Date(currentDate.getTime() - 24 * 60 * 60 * 1000); // One day ago
+
+ render(
+
+ );
+
+ expect(screen.queryByText("Pending Downgrade")).not.toBeInTheDocument();
+ });
+
+ test("doesn't render when isPendingDowngrade is false", () => {
+ const currentDate = new Date();
+ const lastChecked = new Date(currentDate.getTime() - 24 * 60 * 60 * 1000); // One day ago
+
+ render(
+
+ );
+
+ expect(screen.queryByText("Pending Downgrade")).not.toBeInTheDocument();
+ });
+
+ test("closes banner when close button is clicked", async () => {
+ const user = userEvent.setup();
+ const currentDate = new Date();
+ const lastChecked = new Date(currentDate.getTime() - 24 * 60 * 60 * 1000); // One day ago
+
+ render(
+
+ );
+
+ expect(screen.getByText("Pending Downgrade")).toBeInTheDocument();
+
+ // Find and click the close button
+ const closeButton = screen.getByRole("button", { name: "Close" });
+ await user.click(closeButton);
+
+ // Banner should no longer be visible
+ expect(screen.queryByText("Pending Downgrade")).not.toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/picture-selection-response/index.test.tsx b/apps/web/modules/ui/components/picture-selection-response/index.test.tsx
new file mode 100644
index 0000000000..cf1de432f1
--- /dev/null
+++ b/apps/web/modules/ui/components/picture-selection-response/index.test.tsx
@@ -0,0 +1,77 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { PictureSelectionResponse } from "./index";
+
+// Mock next/image because it's not available in the test environment
+vi.mock("next/image", () => ({
+ __esModule: true,
+ default: ({ src, alt, className }: { src: string; alt: string; className: string }) => (
+
+ ),
+}));
+
+describe("PictureSelectionResponse", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockChoices = [
+ {
+ id: "choice1",
+ imageUrl: "https://example.com/image1.jpg",
+ },
+ {
+ id: "choice2",
+ imageUrl: "https://example.com/image2.jpg",
+ },
+ {
+ id: "choice3",
+ imageUrl: "https://example.com/image3.jpg",
+ },
+ ];
+
+ test("renders images for selected choices", () => {
+ const { container } = render(
+
+ );
+
+ const images = container.querySelectorAll("img");
+ expect(images).toHaveLength(2);
+ expect(images[0]).toHaveAttribute("src", "https://example.com/image1.jpg");
+ expect(images[1]).toHaveAttribute("src", "https://example.com/image3.jpg");
+ });
+
+ test("renders nothing when selected is not an array", () => {
+ // @ts-ignore - Testing invalid prop type
+ const { container } = render( );
+ expect(container.firstChild).toBeNull();
+ });
+
+ test("handles expanded layout", () => {
+ const { container } = render(
+
+ );
+
+ const wrapper = container.firstChild as HTMLElement;
+ expect(wrapper).toHaveClass("flex-wrap");
+ });
+
+ test("handles non-expanded layout", () => {
+ const { container } = render(
+
+ );
+
+ const wrapper = container.firstChild as HTMLElement;
+ expect(wrapper).not.toHaveClass("flex-wrap");
+ });
+
+ test("handles choices not in the mapping", () => {
+ const { container } = render(
+
+ );
+
+ const images = container.querySelectorAll("img");
+ expect(images).toHaveLength(1); // Only one valid image should be rendered
+ });
+});
diff --git a/apps/web/modules/ui/components/popover/index.test.tsx b/apps/web/modules/ui/components/popover/index.test.tsx
new file mode 100644
index 0000000000..0b76e6d186
--- /dev/null
+++ b/apps/web/modules/ui/components/popover/index.test.tsx
@@ -0,0 +1,112 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { Popover, PopoverContent, PopoverTrigger } from "./index";
+
+// Mock RadixUI's Portal to make testing easier
+vi.mock("@radix-ui/react-popover", async () => {
+ const actual = await vi.importActual("@radix-ui/react-popover");
+ return {
+ ...actual,
+ Portal: ({ children }: { children: React.ReactNode }) => {children}
,
+ };
+});
+
+describe("Popover", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders the popover with trigger and content", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ Open Popover
+ Popover Content
+
+ );
+
+ // Trigger should be visible
+ const trigger = screen.getByText("Open Popover");
+ expect(trigger).toBeInTheDocument();
+
+ // Content should not be visible initially
+ expect(screen.queryByText("Popover Content")).not.toBeInTheDocument();
+
+ // Click the trigger to open the popover
+ await user.click(trigger);
+
+ // Content should now be visible inside the Portal
+ const portal = screen.getByTestId("portal");
+ expect(portal).toBeInTheDocument();
+ expect(portal).toHaveTextContent("Popover Content");
+ });
+
+ test("passes align and sideOffset props to popover content", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ Open Popover
+
+ Popover Content
+
+
+ );
+
+ // Click the trigger to open the popover
+ await user.click(screen.getByText("Open Popover"));
+
+ // Content should have the align and sideOffset props
+ const content = screen.getByTestId("portal").firstChild as HTMLElement;
+
+ // These attributes are handled by RadixUI internally, so we can't directly test the DOM
+ // but we can verify the component doesn't crash when these props are provided
+ expect(content).toBeInTheDocument();
+ expect(content).toHaveTextContent("Popover Content");
+ });
+
+ test("forwards ref to popover content", async () => {
+ const user = userEvent.setup();
+ const ref = vi.fn();
+
+ render(
+
+ Open Popover
+ Popover Content
+
+ );
+
+ // Click the trigger to open the popover
+ await user.click(screen.getByText("Open Popover"));
+
+ // Ref should have been called - this test is mostly to ensure the component supports refs
+ expect(screen.getByTestId("portal")).toBeInTheDocument();
+ });
+
+ test("closes when clicking outside", async () => {
+ const user = userEvent.setup();
+
+ render(
+ <>
+ Outside
+
+ Open Popover
+ Popover Content
+
+ >
+ );
+
+ // Open the popover
+ await user.click(screen.getByText("Open Popover"));
+ expect(screen.getByTestId("portal")).toBeInTheDocument();
+
+ // Click outside
+ await user.click(screen.getByTestId("outside-element"));
+
+ // This test is more about ensuring the component has the default behavior of closing on outside click
+ // The actual closing is handled by RadixUI, so we can't directly test it without more complex mocking
+ });
+});
diff --git a/apps/web/modules/ui/components/preview-survey/components/tab-option.test.tsx b/apps/web/modules/ui/components/preview-survey/components/tab-option.test.tsx
new file mode 100644
index 0000000000..f53db2b9a1
--- /dev/null
+++ b/apps/web/modules/ui/components/preview-survey/components/tab-option.test.tsx
@@ -0,0 +1,61 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TabOption } from "./tab-option";
+
+describe("TabOption", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders with active state", () => {
+ render(Icon} onClick={() => {}} />);
+
+ const tabElement = screen.getByTestId("test-icon").parentElement;
+ expect(tabElement).toBeInTheDocument();
+ expect(tabElement).toHaveClass("rounded-full");
+ expect(tabElement).toHaveClass("bg-slate-200");
+ expect(screen.getByTestId("test-icon")).toBeInTheDocument();
+ });
+
+ test("renders with inactive state", () => {
+ render(Icon} onClick={() => {}} />);
+
+ const tabElement = screen.getByTestId("test-icon").parentElement;
+ expect(tabElement).toBeInTheDocument();
+ expect(tabElement).not.toHaveClass("rounded-full");
+ expect(tabElement).not.toHaveClass("bg-slate-200");
+ expect(screen.getByTestId("test-icon")).toBeInTheDocument();
+ });
+
+ test("calls onClick handler when clicked", async () => {
+ const handleClick = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+ Icon} onClick={handleClick} />
+ );
+
+ const tabElement = screen.getByTestId("test-icon").parentElement;
+ await user.click(tabElement!);
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+
+ test("renders children (icon) properly", () => {
+ render(
+
+ Nested Icon
+
+ }
+ onClick={() => {}}
+ />
+ );
+
+ expect(screen.getByTestId("complex-icon")).toBeInTheDocument();
+ expect(screen.getByText("Nested Icon")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/preview-survey/index.test.tsx b/apps/web/modules/ui/components/preview-survey/index.test.tsx
new file mode 100644
index 0000000000..7491c95546
--- /dev/null
+++ b/apps/web/modules/ui/components/preview-survey/index.test.tsx
@@ -0,0 +1,356 @@
+import { SurveyType } from "@prisma/client";
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { PreviewSurvey } from "./index";
+
+// Mock dependent components
+vi.mock("@/modules/ui/components/client-logo", () => ({
+ ClientLogo: ({ environmentId, projectLogo, previewSurvey }: any) => (
+
+ {projectLogo ? "Custom Logo" : "Default Logo"}
+
+ ),
+}));
+
+vi.mock("@/modules/ui/components/media-background", () => ({
+ MediaBackground: ({ children, surveyType, styling, isMobilePreview, isEditorView }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("@/modules/ui/components/reset-progress-button", () => ({
+ ResetProgressButton: ({ onClick }: any) => (
+
+ Reset Progress
+
+ ),
+}));
+
+vi.mock("@/modules/ui/components/survey", () => ({
+ SurveyInline: ({
+ survey,
+ isBrandingEnabled,
+ isPreviewMode,
+ getSetQuestionId,
+ onClose,
+ onFinished,
+ languageCode,
+ }: any) => {
+ // Store the setQuestionId function to be used in tests
+ if (getSetQuestionId) {
+ getSetQuestionId((val: string) => {
+ // Just a simple implementation for testing
+ });
+ }
+
+ return (
+
+
+ Close
+
+
+ Finish
+
+
+ );
+ },
+}));
+
+vi.mock("./components/modal", () => ({
+ Modal: ({ children, isOpen, placement, darkOverlay, clickOutsideClose, previewMode }: any) =>
+ isOpen ? (
+
+ {children}
+
+ ) : null,
+}));
+
+vi.mock("./components/tab-option", () => ({
+ TabOption: ({ active, onClick, icon }: any) => (
+
+ {icon}
+
+ ),
+}));
+
+// Mock framer-motion to avoid animation issues in tests
+vi.mock("framer-motion", () => ({
+ motion: {
+ div: ({ children, ...props }: any) =>
{children}
,
+ },
+ Variants: vi.fn(),
+}));
+
+// Mock the icon components
+vi.mock("lucide-react", () => ({
+ ExpandIcon: () =>
Expand ,
+ ShrinkIcon: () =>
Shrink ,
+ MonitorIcon: () =>
Monitor ,
+ SmartphoneIcon: () =>
Smartphone ,
+}));
+
+// Mock the tolgee translation
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+describe("PreviewSurvey", () => {
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ });
+
+ const mockProject = {
+ id: "project-1",
+ name: "Test Project",
+ placement: "bottomRight",
+ darkOverlay: false,
+ clickOutsideClose: true,
+ styling: {
+ roundness: 8,
+ allowStyleOverwrite: false,
+ cardBackgroundColor: {
+ light: "#FFFFFF",
+ },
+ highlightBorderColor: {
+ light: "",
+ },
+ isLogoHidden: false,
+ },
+ inAppSurveyBranding: true,
+ linkSurveyBranding: true,
+ logo: null,
+ } as any;
+
+ const mockEnvironment = {
+ id: "env-1",
+ appSetupCompleted: true,
+ } as any;
+
+ const mockSurvey = {
+ id: "survey-1",
+ name: "Test Survey",
+ type: "app" as SurveyType,
+ welcomeCard: {
+ enabled: true,
+ },
+ questions: [
+ { id: "q1", headline: "Question 1" },
+ { id: "q2", headline: "Question 2" },
+ ],
+ endings: [],
+ styling: {
+ overwriteThemeStyling: false,
+ roundness: 8,
+ cardBackgroundColor: {
+ light: "#FFFFFF",
+ },
+ highlightBorderColor: {
+ light: "",
+ },
+ isLogoHidden: false,
+ },
+ recaptcha: {
+ enabled: false,
+ },
+ } as any;
+
+ test("renders desktop preview mode by default", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("environments.surveys.edit.your_web_app")).toBeInTheDocument();
+ expect(screen.getByTestId("survey-modal")).toBeInTheDocument();
+ expect(screen.getByTestId("survey-inline")).toBeInTheDocument();
+ expect(screen.getByTestId("tab-option-active")).toBeInTheDocument();
+ expect(screen.getByTestId("tab-option-inactive")).toBeInTheDocument();
+ });
+
+ test("switches to mobile preview mode when clicked", async () => {
+ const user = userEvent.setup();
+ render(
+
+ );
+
+ // Initially in desktop mode
+ expect(screen.getByTestId("survey-modal")).toHaveAttribute("data-preview-mode", "desktop");
+
+ // Click on mobile tab
+ const mobileTab = screen.getAllByTestId(/tab-option/)[0];
+ await user.click(mobileTab);
+
+ // Should be in mobile preview mode now
+ expect(screen.getByText("Preview")).toBeInTheDocument();
+ expect(screen.getByTestId("media-background")).toHaveAttribute("data-is-mobile-preview", "true");
+ });
+
+ test("resets survey progress when reset button is clicked", async () => {
+ // Add the modal component to the DOM even after click
+
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ const resetButton = screen.getByTestId("reset-progress-button");
+ await user.click(resetButton);
+
+ // Wait for component to update
+ await vi.waitFor(() => {
+ expect(screen.queryByTestId("survey-inline")).toBeInTheDocument();
+ });
+ });
+
+ test("handles survey completion", async () => {
+ // Add the modal component to the DOM even after click
+
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ // Find and click the finish button
+ const finishButton = screen.getByTestId("finish-survey");
+ await user.click(finishButton);
+
+ // Wait for component to update
+ await new Promise((r) => setTimeout(r, 600));
+
+ // Verify we can find survey elements after completion
+ expect(screen.queryByTestId("survey-inline")).toBeInTheDocument();
+ });
+
+ test("renders fullwidth preview when specified", () => {
+ render(
+
+ );
+
+ // Should render with MediaBackground in desktop mode
+ expect(screen.queryByTestId("survey-modal")).not.toBeInTheDocument();
+ expect(screen.getByTestId("media-background")).toBeInTheDocument();
+ expect(screen.getByTestId("media-background")).toHaveAttribute("data-is-editor-view", "true");
+ });
+
+ test("handles expand/shrink preview", async () => {
+ const user = userEvent.setup();
+
+ // Override the Lucide-react mock for this specific test
+ vi.mock("lucide-react", () => {
+ let isExpanded = false;
+
+ return {
+ ExpandIcon: () => (
+
{
+ isExpanded = true;
+ }}>
+ Expand
+
+ ),
+ ShrinkIcon: () =>
Shrink ,
+ MonitorIcon: () =>
Monitor ,
+ SmartphoneIcon: () =>
Smartphone ,
+ };
+ });
+
+ render(
+
+ );
+
+ // Initially shows expand icon
+ expect(screen.getByTestId("expand-icon")).toBeInTheDocument();
+
+ // Since we can't easily test the full expand/shrink functionality in the test environment,
+ // we'll skip verifying the shrink icon and just make sure the component doesn't crash
+ });
+
+ test("renders with reCAPTCHA enabled when specified", () => {
+ const surveyWithRecaptcha = {
+ ...mockSurvey,
+ recaptcha: {
+ enabled: true,
+ },
+ };
+
+ render(
+
+ );
+
+ // Should render with isSpamProtectionEnabled=true
+ expect(screen.getByTestId("survey-inline")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/preview-survey/index.tsx b/apps/web/modules/ui/components/preview-survey/index.tsx
index 5898c76763..5e232d1a87 100644
--- a/apps/web/modules/ui/components/preview-survey/index.tsx
+++ b/apps/web/modules/ui/components/preview-survey/index.tsx
@@ -246,10 +246,10 @@ export const PreviewSurvey = ({
className="relative flex h-full w-[95%] items-center justify-center rounded-lg border border-slate-300 bg-slate-200">
{previewMode === "mobile" && (
<>
-
+
Preview
-
+
) : (
-
+
{!styling.isLogoHidden && (
)}
@@ -392,7 +392,7 @@ export const PreviewSurvey = ({
styling={styling}
ContentRef={ContentRef as React.RefObject
}
isEditorView>
-
+
{!styling.isLogoHidden && (
)}
diff --git a/apps/web/modules/ui/components/preview-survey/lib/utils.test.ts b/apps/web/modules/ui/components/preview-survey/lib/utils.test.ts
new file mode 100644
index 0000000000..6892c34a98
--- /dev/null
+++ b/apps/web/modules/ui/components/preview-survey/lib/utils.test.ts
@@ -0,0 +1,36 @@
+import "@testing-library/jest-dom/vitest";
+import { describe, expect, test } from "vitest";
+import { getPlacementStyle } from "./utils";
+
+describe("getPlacementStyle", () => {
+ test("returns correct style for bottomRight placement", () => {
+ const style = getPlacementStyle("bottomRight");
+ expect(style).toBe("bottom-3 sm:right-3");
+ });
+
+ test("returns correct style for topRight placement", () => {
+ const style = getPlacementStyle("topRight");
+ expect(style).toBe("sm:top-6 sm:right-6");
+ });
+
+ test("returns correct style for topLeft placement", () => {
+ const style = getPlacementStyle("topLeft");
+ expect(style).toBe("sm:top-6 sm:left-6");
+ });
+
+ test("returns correct style for bottomLeft placement", () => {
+ const style = getPlacementStyle("bottomLeft");
+ expect(style).toBe("bottom-3 sm:left-3");
+ });
+
+ test("returns correct style for center placement", () => {
+ const style = getPlacementStyle("center");
+ expect(style).toBe("top-1/2 left-1/2 transform !-translate-x-1/2 -translate-y-1/2");
+ });
+
+ test("returns default style for invalid placement", () => {
+ // @ts-ignore - Testing with invalid input
+ const style = getPlacementStyle("invalidPlacement");
+ expect(style).toBe("bottom-3 sm:right-3");
+ });
+});
diff --git a/apps/web/modules/ui/components/pro-badge/index.test.tsx b/apps/web/modules/ui/components/pro-badge/index.test.tsx
new file mode 100644
index 0000000000..e15e891c7c
--- /dev/null
+++ b/apps/web/modules/ui/components/pro-badge/index.test.tsx
@@ -0,0 +1,50 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { ProBadge } from "./index";
+
+// Mock lucide-react's CrownIcon
+vi.mock("lucide-react", () => ({
+ CrownIcon: () =>
,
+}));
+
+describe("ProBadge", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders the badge with correct elements", () => {
+ render(
);
+
+ // Check for container with correct classes
+ const badgeContainer = screen.getByText("PRO").closest("div");
+ expect(badgeContainer).toBeInTheDocument();
+ expect(badgeContainer).toHaveClass("ml-2");
+ expect(badgeContainer).toHaveClass("flex");
+ expect(badgeContainer).toHaveClass("items-center");
+ expect(badgeContainer).toHaveClass("justify-center");
+ expect(badgeContainer).toHaveClass("rounded-lg");
+ expect(badgeContainer).toHaveClass("border");
+ expect(badgeContainer).toHaveClass("border-slate-200");
+ expect(badgeContainer).toHaveClass("bg-slate-100");
+ expect(badgeContainer).toHaveClass("p-0.5");
+ expect(badgeContainer).toHaveClass("text-slate-500");
+ });
+
+ test("contains crown icon", () => {
+ render(
);
+
+ const crownIcon = screen.getByTestId("crown-icon");
+ expect(crownIcon).toBeInTheDocument();
+ });
+
+ test("displays PRO text", () => {
+ render(
);
+
+ const proText = screen.getByText("PRO");
+ expect(proText).toBeInTheDocument();
+ expect(proText.tagName.toLowerCase()).toBe("span");
+ expect(proText).toHaveClass("ml-1");
+ expect(proText).toHaveClass("text-xs");
+ });
+});
diff --git a/apps/web/modules/ui/components/question-toggle-table/index.test.tsx b/apps/web/modules/ui/components/question-toggle-table/index.test.tsx
new file mode 100644
index 0000000000..6d5522f689
--- /dev/null
+++ b/apps/web/modules/ui/components/question-toggle-table/index.test.tsx
@@ -0,0 +1,322 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TSurvey } from "@formbricks/types/surveys/types";
+import { QuestionToggleTable } from "./index";
+
+// Mock the Switch component
+vi.mock("@/modules/ui/components/switch", () => ({
+ Switch: ({ checked, onCheckedChange, disabled }: any) => (
+
onCheckedChange(!checked)}
+ disabled={disabled}>
+ {checked ? "On" : "Off"}
+
+ ),
+}));
+
+// Mock the QuestionFormInput component
+vi.mock("@/modules/survey/components/question-form-input", () => ({
+ QuestionFormInput: ({ id, value, updateQuestion, questionIdx, selectedLanguageCode }: any) => (
+
{
+ const updatedAttributes: any = {};
+ const fieldId = id.split(".")[0];
+ const attributeName = id.split(".")[1];
+
+ updatedAttributes[fieldId] = {
+ show: true,
+ required: false,
+ placeholder: {
+ [selectedLanguageCode]: e.target.value,
+ },
+ };
+
+ updateQuestion(questionIdx, updatedAttributes);
+ }}
+ />
+ ),
+}));
+
+// Mock tolgee
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+describe("QuestionToggleTable", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockFields = [
+ {
+ id: "street",
+ show: true,
+ required: true,
+ label: "Street",
+ placeholder: { default: "Enter your street" },
+ },
+ {
+ id: "city",
+ show: true,
+ required: false,
+ label: "City",
+ placeholder: { default: "Enter your city" },
+ },
+ ];
+
+ const mockSurvey: TSurvey = {
+ id: "survey-1",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ name: "Test Survey",
+ type: "web",
+ environmentId: "env-1",
+ status: "draft",
+ questions: [
+ {
+ id: "question-1",
+ type: "address",
+ headline: "Your address",
+ required: true,
+ street: {
+ show: true,
+ required: true,
+ placeholder: { default: "Street" },
+ },
+ city: {
+ show: true,
+ required: false,
+ placeholder: { default: "City" },
+ },
+ },
+ ],
+ welcomeCard: {
+ enabled: false,
+ },
+ thankYouCard: {
+ enabled: false,
+ },
+ displayProgress: false,
+ progressBar: {
+ display: false,
+ },
+ styling: {},
+ autoComplete: false,
+ closeOnDate: null,
+ recaptcha: {
+ enabled: false,
+ },
+ } as unknown as TSurvey;
+
+ test("renders address fields correctly", () => {
+ const updateQuestionMock = vi.fn();
+
+ render(
+
{}}
+ locale={"en-US"}
+ />
+ );
+
+ // Check table headers
+ expect(screen.getByText("environments.surveys.edit.address_fields")).toBeInTheDocument();
+ expect(screen.getByText("common.show")).toBeInTheDocument();
+ expect(screen.getByText("environments.surveys.edit.required")).toBeInTheDocument();
+ expect(screen.getByText("common.label")).toBeInTheDocument();
+
+ // Check field labels
+ expect(screen.getByText("Street")).toBeInTheDocument();
+ expect(screen.getByText("City")).toBeInTheDocument();
+
+ // Check switches are rendered with correct state
+ const streetShowSwitch = screen.getAllByTestId("switch-on")[0];
+ const streetRequiredSwitch = screen.getAllByTestId("switch-on")[1];
+ const cityShowSwitch = screen.getAllByTestId("switch-on")[2];
+ const cityRequiredSwitch = screen.getByTestId("switch-off");
+
+ expect(streetShowSwitch).toBeInTheDocument();
+ expect(streetRequiredSwitch).toBeInTheDocument();
+ expect(cityShowSwitch).toBeInTheDocument();
+ expect(cityRequiredSwitch).toBeInTheDocument();
+
+ // Check inputs are rendered
+ expect(screen.getByTestId("input-street.placeholder")).toBeInTheDocument();
+ expect(screen.getByTestId("input-city.placeholder")).toBeInTheDocument();
+ });
+
+ test("renders contact fields correctly", () => {
+ const updateQuestionMock = vi.fn();
+
+ render(
+ {}}
+ locale={"en-US"}
+ />
+ );
+
+ expect(screen.getByText("environments.surveys.edit.contact_fields")).toBeInTheDocument();
+ });
+
+ test("handles show toggle", async () => {
+ const updateQuestionMock = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+ {}}
+ locale={"en-US"}
+ />
+ );
+
+ // Toggle the city show switch
+ const cityShowSwitch = screen.getAllByTestId("switch-on")[2];
+ await user.click(cityShowSwitch);
+
+ // Check that updateQuestion was called with correct parameters
+ expect(updateQuestionMock).toHaveBeenCalledWith(0, {
+ city: {
+ show: false,
+ required: false,
+ placeholder: { default: "Enter your city" },
+ },
+ });
+ });
+
+ test("handles required toggle", async () => {
+ const updateQuestionMock = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+ {}}
+ locale={"en-US"}
+ />
+ );
+
+ // Toggle the city required switch
+ const cityRequiredSwitch = screen.getByTestId("switch-off");
+ await user.click(cityRequiredSwitch);
+
+ // Check that updateQuestion was called with correct parameters
+ expect(updateQuestionMock).toHaveBeenCalledWith(0, {
+ city: {
+ show: true,
+ required: true,
+ placeholder: { default: "Enter your city" },
+ },
+ });
+ });
+
+ test("disables show toggle when it's the last visible field", async () => {
+ const fieldsWithOnlyOneVisible = [
+ {
+ id: "street",
+ show: true,
+ required: false,
+ label: "Street",
+ placeholder: { default: "Enter your street" },
+ },
+ {
+ id: "city",
+ show: false,
+ required: false,
+ label: "City",
+ placeholder: { default: "Enter your city" },
+ },
+ ];
+
+ render(
+ {}}
+ selectedLanguageCode="default"
+ setSelectedLanguageCode={() => {}}
+ locale={"en-US"}
+ />
+ );
+
+ // The street show toggle should be disabled
+ const streetShowSwitch = screen.getByTestId("switch-on");
+ expect(streetShowSwitch).toHaveAttribute("data-disabled", "true");
+ expect(streetShowSwitch).toBeDisabled();
+ });
+
+ test("disables required toggle when field is not shown", async () => {
+ const fieldsWithHiddenField = [
+ {
+ id: "street",
+ show: true,
+ required: false,
+ label: "Street",
+ placeholder: { default: "Enter your street" },
+ },
+ {
+ id: "city",
+ show: false,
+ required: false,
+ label: "City",
+ placeholder: { default: "Enter your city" },
+ },
+ ];
+
+ render(
+ {}}
+ selectedLanguageCode="default"
+ setSelectedLanguageCode={() => {}}
+ locale={"en-US"}
+ />
+ );
+
+ // The city required toggle should be disabled
+ const requiredSwitches = screen.getAllByTestId("switch-off");
+ const cityRequiredSwitch = requiredSwitches[requiredSwitches.length - 1]; // Last one should be city's required switch
+ expect(cityRequiredSwitch).toHaveAttribute("data-disabled", "true");
+ expect(cityRequiredSwitch).toBeDisabled();
+ });
+});
diff --git a/apps/web/modules/ui/components/radio-group/index.test.tsx b/apps/web/modules/ui/components/radio-group/index.test.tsx
new file mode 100644
index 0000000000..679dd9408f
--- /dev/null
+++ b/apps/web/modules/ui/components/radio-group/index.test.tsx
@@ -0,0 +1,134 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { RadioGroup, RadioGroupItem } from "./index";
+
+describe("RadioGroup", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders radio group with items", () => {
+ render(
+
+
+
+ Option 1
+
+
+
+ Option 2
+
+
+ );
+
+ expect(screen.getByText("Option 1")).toBeInTheDocument();
+ expect(screen.getByText("Option 2")).toBeInTheDocument();
+ expect(screen.getByLabelText("Option 1")).toBeInTheDocument();
+ expect(screen.getByLabelText("Option 2")).toBeInTheDocument();
+ });
+
+ test("selects default value", () => {
+ render(
+
+
+
+ Option 1
+
+
+
+ Option 2
+
+
+ );
+
+ const option1 = screen.getByLabelText("Option 1");
+ const option2 = screen.getByLabelText("Option 2");
+
+ expect(option1).toBeChecked();
+ expect(option2).not.toBeChecked();
+ });
+
+ test("changes selection when clicking on a different option", async () => {
+ const user = userEvent.setup();
+ const handleValueChange = vi.fn();
+
+ render(
+
+
+
+ Option 1
+
+
+
+ Option 2
+
+
+ );
+
+ const option2 = screen.getByLabelText("Option 2");
+ await user.click(option2);
+
+ expect(handleValueChange).toHaveBeenCalledWith("option2");
+ });
+
+ test("renders disabled radio items", async () => {
+ const user = userEvent.setup();
+ const handleValueChange = vi.fn();
+
+ render(
+
+
+
+ Option 1
+
+
+
+ Option 2 (Disabled)
+
+
+ );
+
+ const option2 = screen.getByLabelText("Option 2 (Disabled)");
+ expect(option2).toBeDisabled();
+
+ await user.click(option2);
+ expect(handleValueChange).not.toHaveBeenCalled();
+ });
+
+ test("applies custom className to RadioGroup", () => {
+ render(
+
+
+
+ Option 1
+
+
+ );
+
+ const radioGroup = screen.getByRole("radiogroup");
+ expect(radioGroup).toHaveClass("custom-class");
+ expect(radioGroup).toHaveClass("grid");
+ expect(radioGroup).toHaveClass("gap-x-3");
+ });
+
+ test("applies custom className to RadioGroupItem", () => {
+ render(
+
+
+
+ Option 1
+
+
+ );
+
+ const radioItem = screen.getByLabelText("Option 1");
+ expect(radioItem).toHaveClass("custom-item-class");
+ expect(radioItem).toHaveClass("h-4");
+ expect(radioItem).toHaveClass("w-4");
+ expect(radioItem).toHaveClass("rounded-full");
+ expect(radioItem).toHaveClass("border");
+ expect(radioItem).toHaveClass("border-slate-300");
+ });
+});
diff --git a/apps/web/modules/ui/components/ranking-response/index.test.tsx b/apps/web/modules/ui/components/ranking-response/index.test.tsx
new file mode 100644
index 0000000000..2da2a20943
--- /dev/null
+++ b/apps/web/modules/ui/components/ranking-response/index.test.tsx
@@ -0,0 +1,82 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { RankingResponse } from "./index";
+
+describe("RankingResponse", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders ranked items correctly", () => {
+ const rankedItems = ["Apple", "Banana", "Cherry"];
+
+ render( );
+
+ expect(screen.getByText("#1")).toBeInTheDocument();
+ expect(screen.getByText("#2")).toBeInTheDocument();
+ expect(screen.getByText("#3")).toBeInTheDocument();
+ expect(screen.getByText("Apple")).toBeInTheDocument();
+ expect(screen.getByText("Banana")).toBeInTheDocument();
+ expect(screen.getByText("Cherry")).toBeInTheDocument();
+ });
+
+ test("applies expanded layout", () => {
+ const rankedItems = ["Apple", "Banana"];
+
+ const { container } = render( );
+
+ const parentDiv = container.firstChild;
+ expect(parentDiv).not.toHaveClass("flex");
+ expect(parentDiv).not.toHaveClass("space-x-2");
+ });
+
+ test("applies non-expanded layout", () => {
+ const rankedItems = ["Apple", "Banana"];
+
+ const { container } = render( );
+
+ const parentDiv = container.firstChild;
+ expect(parentDiv).toHaveClass("flex");
+ expect(parentDiv).toHaveClass("space-x-2");
+ });
+
+ test("handles empty values", () => {
+ const rankedItems = ["Apple", "", "Cherry"];
+
+ render( );
+
+ expect(screen.getByText("#1")).toBeInTheDocument();
+ expect(screen.getByText("#3")).toBeInTheDocument();
+ expect(screen.getByText("Apple")).toBeInTheDocument();
+ expect(screen.getByText("Cherry")).toBeInTheDocument();
+ expect(screen.queryByText("#2")).not.toBeInTheDocument();
+ });
+
+ test("displays items in the correct order", () => {
+ const rankedItems = ["First", "Second", "Third"];
+
+ render( );
+
+ const rankNumbers = screen.getAllByText(/^#\d$/);
+ const rankItems = screen.getAllByText(/(First|Second|Third)/);
+
+ expect(rankNumbers[0].textContent).toBe("#1");
+ expect(rankItems[0].textContent).toBe("First");
+
+ expect(rankNumbers[1].textContent).toBe("#2");
+ expect(rankItems[1].textContent).toBe("Second");
+
+ expect(rankNumbers[2].textContent).toBe("#3");
+ expect(rankItems[2].textContent).toBe("Third");
+ });
+
+ test("renders with RTL support", () => {
+ const rankedItems = ["תפוח", "בננה", "דובדבן"];
+
+ const { container } = render( );
+
+ const parentDiv = container.firstChild as HTMLElement;
+ expect(parentDiv).toHaveAttribute("dir", "auto");
+ });
+});
diff --git a/apps/web/modules/ui/components/ranking-response/index.tsx b/apps/web/modules/ui/components/ranking-response/index.tsx
index 48cada6082..ac75ad566d 100644
--- a/apps/web/modules/ui/components/ranking-response/index.tsx
+++ b/apps/web/modules/ui/components/ranking-response/index.tsx
@@ -5,7 +5,7 @@ interface RankingResponseProps {
isExpanded: boolean;
}
-export const RankingRespone = ({ value, isExpanded }: RankingResponseProps) => {
+export const RankingResponse = ({ value, isExpanded }: RankingResponseProps) => {
return (
{value.map(
diff --git a/apps/web/modules/ui/components/rating-response/index.test.tsx b/apps/web/modules/ui/components/rating-response/index.test.tsx
new file mode 100644
index 0000000000..ecea358b82
--- /dev/null
+++ b/apps/web/modules/ui/components/rating-response/index.test.tsx
@@ -0,0 +1,71 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { RatingResponse } from "./index";
+
+// Mock the RatingSmiley component
+vi.mock("@/modules/analysis/components/RatingSmiley", () => ({
+ RatingSmiley: ({ active, idx, range, addColors }: any) => (
+
+ Smiley Rating
+
+ ),
+}));
+
+describe("RatingResponse", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders null when answer is not a number", () => {
+ const { container } = render(
);
+ expect(container.firstChild).toBeNull();
+ });
+
+ test("returns raw answer when scale or range is undefined", () => {
+ const { container } = render(
);
+ expect(container).toHaveTextContent("3");
+ });
+
+ test("renders smiley rating correctly", () => {
+ render(
);
+
+ const smiley = screen.getByTestId("rating-smiley");
+ expect(smiley).toBeInTheDocument();
+ expect(smiley).toHaveAttribute("data-active", "false");
+ expect(smiley).toHaveAttribute("data-idx", "2"); // 0-based index for rating 3
+ expect(smiley).toHaveAttribute("data-range", "5");
+ expect(smiley).toHaveAttribute("data-add-colors", "false");
+ });
+
+ test("renders smiley rating with colors", () => {
+ render(
);
+
+ const smiley = screen.getByTestId("rating-smiley");
+ expect(smiley).toBeInTheDocument();
+ expect(smiley).toHaveAttribute("data-add-colors", "true");
+ });
+
+ test("renders number rating correctly", () => {
+ const { container } = render(
);
+ expect(container).toHaveTextContent("7");
+ });
+
+ test("handles full rating correctly", () => {
+ render(
);
+
+ const stars = document.querySelectorAll("svg");
+ expect(stars).toHaveLength(5);
+
+ // All stars should be filled
+ for (let i = 0; i < 5; i++) {
+ expect(stars[i].getAttribute("fill")).toBe("rgb(250 204 21)");
+ expect(stars[i]).toHaveClass("text-yellow-400");
+ }
+ });
+});
diff --git a/apps/web/modules/ui/components/reset-progress-button/index.test.tsx b/apps/web/modules/ui/components/reset-progress-button/index.test.tsx
new file mode 100644
index 0000000000..0cba5022f3
--- /dev/null
+++ b/apps/web/modules/ui/components/reset-progress-button/index.test.tsx
@@ -0,0 +1,53 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { ResetProgressButton } from "./index";
+
+// Mock tolgee
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => (key === "common.restart" ? "Restart" : key),
+ }),
+}));
+
+// Mock lucide-react
+vi.mock("lucide-react", () => ({
+ Repeat2: () =>
,
+}));
+
+describe("ResetProgressButton", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders button with correct text", () => {
+ render(
{}} />);
+
+ expect(screen.getByRole("button")).toBeInTheDocument();
+ expect(screen.getByText("Restart")).toBeInTheDocument();
+ expect(screen.getByTestId("repeat-icon")).toBeInTheDocument();
+ });
+
+ test("button has correct styling", () => {
+ render( {}} />);
+
+ const button = screen.getByRole("button");
+ expect(button).toHaveClass("h-fit");
+ expect(button).toHaveClass("bg-white");
+ expect(button).toHaveClass("text-slate-500");
+ expect(button).toHaveClass("px-2");
+ expect(button).toHaveClass("py-0");
+ });
+
+ test("calls onClick handler when clicked", async () => {
+ const handleClick = vi.fn();
+ const user = userEvent.setup();
+
+ render( );
+
+ await user.click(screen.getByRole("button"));
+
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/apps/web/modules/ui/components/response-badges/index.test.tsx b/apps/web/modules/ui/components/response-badges/index.test.tsx
new file mode 100644
index 0000000000..d52550c597
--- /dev/null
+++ b/apps/web/modules/ui/components/response-badges/index.test.tsx
@@ -0,0 +1,68 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { ResponseBadges } from "./index";
+
+describe("ResponseBadges", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders string items correctly", () => {
+ const items = ["Apple", "Banana", "Cherry"];
+ render( );
+
+ expect(screen.getByText("Apple")).toBeInTheDocument();
+ expect(screen.getByText("Banana")).toBeInTheDocument();
+ expect(screen.getByText("Cherry")).toBeInTheDocument();
+
+ const badges = screen.getAllByText(/Apple|Banana|Cherry/);
+ expect(badges).toHaveLength(3);
+
+ badges.forEach((badge) => {
+ expect(badge.closest("span")).toHaveClass("bg-slate-200");
+ expect(badge.closest("span")).toHaveClass("rounded-md");
+ expect(badge.closest("span")).toHaveClass("px-2");
+ expect(badge.closest("span")).toHaveClass("py-1");
+ expect(badge.closest("span")).toHaveClass("font-medium");
+ });
+ });
+
+ test("renders number items correctly", () => {
+ const items = [1, 2, 3];
+ render( );
+
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ });
+
+ test("applies expanded layout when isExpanded=true", () => {
+ const items = ["Apple", "Banana", "Cherry"];
+
+ const { container } = render( );
+
+ const wrapper = container.firstChild;
+ expect(wrapper).toHaveClass("flex-wrap");
+ });
+
+ test("does not apply expanded layout when isExpanded=false", () => {
+ const items = ["Apple", "Banana", "Cherry"];
+
+ const { container } = render( );
+
+ const wrapper = container.firstChild;
+ expect(wrapper).not.toHaveClass("flex-wrap");
+ });
+
+ test("applies default styles correctly", () => {
+ const items = ["Apple"];
+
+ const { container } = render( );
+
+ const wrapper = container.firstChild;
+ expect(wrapper).toHaveClass("my-1");
+ expect(wrapper).toHaveClass("flex");
+ expect(wrapper).toHaveClass("gap-2");
+ });
+});
diff --git a/apps/web/modules/ui/components/save-as-new-segment-modal/index.test.tsx b/apps/web/modules/ui/components/save-as-new-segment-modal/index.test.tsx
new file mode 100644
index 0000000000..6e68be88a8
--- /dev/null
+++ b/apps/web/modules/ui/components/save-as-new-segment-modal/index.test.tsx
@@ -0,0 +1,230 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { SaveAsNewSegmentModal } from "./index";
+
+// Mock react-hook-form
+vi.mock("react-hook-form", () => ({
+ useForm: () => ({
+ register: vi.fn().mockImplementation((name) => ({
+ name,
+ onChange: vi.fn(),
+ onBlur: vi.fn(),
+ ref: vi.fn(),
+ })),
+ handleSubmit: vi.fn().mockImplementation((fn) => (data) => {
+ fn(data);
+ return false;
+ }),
+ formState: { errors: {} },
+ setValue: vi.fn(),
+ }),
+}));
+
+// Mock react-hot-toast
+vi.mock("react-hot-toast", () => ({
+ default: {
+ success: vi.fn(),
+ error: vi.fn(),
+ },
+}));
+
+// Mock lucide-react
+vi.mock("lucide-react", () => ({
+ UsersIcon: () =>
,
+}));
+
+// Mock Modal component
+vi.mock("@/modules/ui/components/modal", () => ({
+ Modal: ({ open, setOpen, noPadding, children }) => {
+ if (!open) return null;
+ return (
+
+ setOpen(false)}>
+ Close
+
+ {children}
+
+ );
+ },
+}));
+
+// Mock Button component
+vi.mock("@/modules/ui/components/button", () => ({
+ Button: ({ children, variant, onClick, type, loading }) => (
+
+ {children}
+
+ ),
+}));
+
+// Mock Input component
+vi.mock("@/modules/ui/components/input", () => ({
+ Input: (props) => ,
+}));
+
+// Mock the useTranslate hook
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key) => {
+ const translations = {
+ "environments.segments.save_as_new_segment": "Save as New Segment",
+ "environments.segments.save_your_filters_as_a_segment_to_use_it_in_other_surveys":
+ "Save your filters as a segment to use it in other surveys",
+ "common.name": "Name",
+ "environments.segments.ex_power_users": "Ex: Power Users",
+ "common.description": "Description",
+ "environments.segments.most_active_users_in_the_last_30_days":
+ "Most active users in the last 30 days",
+ "common.cancel": "Cancel",
+ "common.save": "Save",
+ "environments.segments.segment_created_successfully": "Segment created successfully",
+ "environments.segments.segment_updated_successfully": "Segment updated successfully",
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
+
+describe("SaveAsNewSegmentModal", () => {
+ afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+ });
+
+ const mockProps = {
+ open: true,
+ setOpen: vi.fn(),
+ localSurvey: {
+ id: "survey1",
+ environmentId: "env1",
+ } as any,
+ segment: {
+ id: "segment1",
+ isPrivate: false,
+ filters: [{ id: "filter1" }],
+ } as any,
+ setSegment: vi.fn(),
+ setIsSegmentEditorOpen: vi.fn(),
+ onCreateSegment: vi.fn().mockResolvedValue({ id: "newSegment" }),
+ onUpdateSegment: vi.fn().mockResolvedValue({ id: "updatedSegment" }),
+ };
+
+ test("renders the modal when open is true", () => {
+ render( );
+
+ expect(screen.getByTestId("modal")).toBeInTheDocument();
+ expect(screen.getByText("Save as New Segment")).toBeInTheDocument();
+ expect(screen.getByText("Save your filters as a segment to use it in other surveys")).toBeInTheDocument();
+ expect(screen.getByTestId("users-icon")).toBeInTheDocument();
+ });
+
+ test("doesn't render when open is false", () => {
+ render( );
+
+ expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
+ });
+
+ test("renders form fields correctly", () => {
+ render( );
+
+ expect(screen.getByText("Name")).toBeInTheDocument();
+ expect(screen.getByTestId("input-title")).toBeInTheDocument();
+ expect(screen.getByText("Description")).toBeInTheDocument();
+ expect(screen.getByTestId("input-description")).toBeInTheDocument();
+ expect(screen.getByText("Cancel")).toBeInTheDocument();
+ expect(screen.getByText("Save")).toBeInTheDocument();
+ });
+
+ test("calls setOpen with false when close button is clicked", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ await user.click(screen.getByTestId("modal-close"));
+
+ expect(mockProps.setOpen).toHaveBeenCalledWith(false);
+ });
+
+ test("calls setOpen with false when cancel button is clicked", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ await user.click(screen.getByText("Cancel"));
+
+ expect(mockProps.setOpen).toHaveBeenCalledWith(false);
+ });
+
+ test("calls onCreateSegment when form is submitted with new segment", async () => {
+ const user = userEvent.setup();
+ const createProps = {
+ ...mockProps,
+ segment: {
+ ...mockProps.segment,
+ id: "temp", // indicates a new segment
+ },
+ };
+
+ render( );
+
+ // Submit the form
+ await user.click(screen.getByText("Save"));
+
+ // Check that onCreateSegment was called
+ expect(createProps.onCreateSegment).toHaveBeenCalled();
+ expect(createProps.setSegment).toHaveBeenCalled();
+ expect(createProps.setIsSegmentEditorOpen).toHaveBeenCalledWith(false);
+ expect(createProps.setOpen).toHaveBeenCalledWith(false);
+ });
+
+ test("calls onUpdateSegment when form is submitted with an existing private segment", async () => {
+ const user = userEvent.setup();
+ const updateProps = {
+ ...mockProps,
+ segment: {
+ ...mockProps.segment,
+ isPrivate: true,
+ },
+ };
+
+ render( );
+
+ // Submit the form
+ await user.click(screen.getByText("Save"));
+
+ // Check that onUpdateSegment was called
+ expect(updateProps.onUpdateSegment).toHaveBeenCalled();
+ expect(updateProps.setSegment).toHaveBeenCalled();
+ expect(updateProps.setIsSegmentEditorOpen).toHaveBeenCalledWith(false);
+ expect(updateProps.setOpen).toHaveBeenCalledWith(false);
+ });
+
+ test("shows loading state on button during submission", async () => {
+ // Use a delayed promise to check loading state
+ const delayedPromise = new Promise((resolve) => {
+ setTimeout(() => resolve({ id: "newSegment" }), 100);
+ });
+
+ const loadingProps = {
+ ...mockProps,
+ segment: {
+ ...mockProps.segment,
+ id: "temp",
+ },
+ onCreateSegment: vi.fn().mockReturnValue(delayedPromise),
+ };
+
+ render( );
+
+ // Submit the form
+ await userEvent.click(screen.getByText("Save"));
+
+ // Button should show loading state
+ const saveButton = screen.getByTestId("button-primary");
+ expect(saveButton).toHaveAttribute("data-loading", "true");
+ });
+});
diff --git a/apps/web/modules/ui/components/search-bar/index.test.tsx b/apps/web/modules/ui/components/search-bar/index.test.tsx
new file mode 100644
index 0000000000..3a6bd00b04
--- /dev/null
+++ b/apps/web/modules/ui/components/search-bar/index.test.tsx
@@ -0,0 +1,45 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { SearchBar } from "./index";
+
+// Mock lucide-react
+vi.mock("lucide-react", () => ({
+ Search: () =>
,
+}));
+
+describe("SearchBar", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders with default placeholder", () => {
+ render( {}} />);
+
+ expect(screen.getByPlaceholderText("Search by survey name")).toBeInTheDocument();
+ expect(screen.getByTestId("search-icon")).toBeInTheDocument();
+ });
+
+ test("renders with custom placeholder", () => {
+ render( {}} placeholder="Custom placeholder" />);
+
+ expect(screen.getByPlaceholderText("Custom placeholder")).toBeInTheDocument();
+ });
+
+ test("displays the provided value", () => {
+ render( {}} />);
+
+ const input = screen.getByPlaceholderText("Search by survey name") as HTMLInputElement;
+ expect(input.value).toBe("test query");
+ });
+
+ test("applies custom className", () => {
+ const { container } = render( {}} className="custom-class" />);
+
+ const searchBarContainer = container.firstChild as HTMLElement;
+ expect(searchBarContainer).toHaveClass("custom-class");
+ expect(searchBarContainer).toHaveClass("flex");
+ expect(searchBarContainer).toHaveClass("h-8");
+ });
+});
diff --git a/apps/web/modules/ui/components/secondary-navigation/index.test.tsx b/apps/web/modules/ui/components/secondary-navigation/index.test.tsx
new file mode 100644
index 0000000000..f48ea19074
--- /dev/null
+++ b/apps/web/modules/ui/components/secondary-navigation/index.test.tsx
@@ -0,0 +1,67 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { SecondaryNavigation } from "./index";
+
+// Mock next/link
+vi.mock("next/link", () => ({
+ __esModule: true,
+ default: ({ children, href, onClick }: any) => (
+
+ {children}
+
+ ),
+}));
+
+describe("SecondaryNavigation", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockNavigation = [
+ { id: "tab1", label: "Tab 1", href: "/tab1" },
+ { id: "tab2", label: "Tab 2", href: "/tab2" },
+ { id: "tab3", label: "Tab 3", onClick: vi.fn() },
+ { id: "tab4", label: "Hidden Tab", href: "/tab4", hidden: true },
+ ];
+
+ test("renders navigation items correctly", () => {
+ render( );
+
+ // Visible tabs
+ expect(screen.getByText("Tab 1")).toBeInTheDocument();
+ expect(screen.getByText("Tab 2")).toBeInTheDocument();
+ expect(screen.getByText("Tab 3")).toBeInTheDocument();
+
+ // Hidden tab
+ expect(screen.queryByText("Hidden Tab")).not.toBeInTheDocument();
+ });
+
+ test("renders links for items with href", () => {
+ render( );
+
+ const links = screen.getAllByTestId("mock-link");
+ expect(links).toHaveLength(2); // tab1 and tab2
+
+ expect(links[0]).toHaveAttribute("href", "/tab1");
+ expect(links[1]).toHaveAttribute("href", "/tab2");
+ });
+
+ test("renders buttons for items without href", () => {
+ render( );
+
+ const button = screen.getByRole("button", { name: "Tab 3" });
+ expect(button).toBeInTheDocument();
+ });
+
+ test("calls onClick function when button is clicked", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ const button = screen.getByRole("button", { name: "Tab 3" });
+ await user.click(button);
+
+ expect(mockNavigation[2].onClick).toHaveBeenCalled();
+ });
+});
diff --git a/apps/web/modules/ui/components/segment-title/index.test.tsx b/apps/web/modules/ui/components/segment-title/index.test.tsx
new file mode 100644
index 0000000000..a2ef2f4189
--- /dev/null
+++ b/apps/web/modules/ui/components/segment-title/index.test.tsx
@@ -0,0 +1,58 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { SegmentTitle } from "./index";
+
+// Mock lucide-react icon
+vi.mock("lucide-react", () => ({
+ UsersIcon: () =>
,
+}));
+
+// Mock tolgee
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) =>
+ key === "environments.surveys.edit.send_survey_to_audience_who_match"
+ ? "Send survey to audience who match the following attributes:"
+ : key,
+ }),
+}));
+
+describe("SegmentTitle", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders with title and description", () => {
+ render( );
+
+ expect(screen.getByText("Test Segment")).toBeInTheDocument();
+ expect(screen.getByText("Test Description")).toBeInTheDocument();
+ expect(screen.getByTestId("users-icon")).toBeInTheDocument();
+ });
+
+ test("renders with title and no description", () => {
+ render( );
+
+ expect(screen.getByText("Test Segment")).toBeInTheDocument();
+ expect(screen.getByTestId("users-icon")).toBeInTheDocument();
+ });
+
+ test("renders private segment text when isPrivate is true", () => {
+ render( );
+
+ expect(
+ screen.getByText("Send survey to audience who match the following attributes:")
+ ).toBeInTheDocument();
+ expect(screen.queryByText("Test Segment")).not.toBeInTheDocument();
+ expect(screen.queryByText("Test Description")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("users-icon")).not.toBeInTheDocument();
+ });
+
+ test("renders correctly with null description", () => {
+ render( );
+
+ expect(screen.getByText("Test Segment")).toBeInTheDocument();
+ expect(screen.getByTestId("users-icon")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/select/index.test.tsx b/apps/web/modules/ui/components/select/index.test.tsx
new file mode 100644
index 0000000000..b65f413e11
--- /dev/null
+++ b/apps/web/modules/ui/components/select/index.test.tsx
@@ -0,0 +1,85 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+} from "./index";
+
+// Mock radix-ui portal to make testing easier
+vi.mock("@radix-ui/react-select", async () => {
+ const actual = await vi.importActual("@radix-ui/react-select");
+ return {
+ ...actual,
+ Portal: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ };
+});
+
+describe("Select", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders the select trigger correctly", () => {
+ render(
+
+
+
+
+
+ );
+
+ const trigger = screen.getByText("Select an option");
+ expect(trigger).toBeInTheDocument();
+ expect(trigger.closest("button")).toHaveClass("border-slate-300");
+ expect(screen.getByRole("combobox")).toBeInTheDocument();
+ });
+
+ test("renders select trigger without arrow when hideArrow is true", () => {
+ render(
+
+
+
+
+
+ );
+
+ const chevronIcon = document.querySelector(".opacity-50");
+ expect(chevronIcon).not.toBeInTheDocument();
+ });
+
+ test("renders select trigger with arrow by default", () => {
+ render(
+
+
+
+
+
+ );
+
+ const chevronIcon = document.querySelector(".opacity-50");
+ expect(chevronIcon).toBeInTheDocument();
+ });
+
+ test("applies custom className to select trigger", () => {
+ render(
+
+
+
+
+
+ );
+
+ const trigger = screen.getByRole("combobox");
+ expect(trigger).toHaveClass("custom-class");
+ });
+});
diff --git a/apps/web/modules/ui/components/settings-id/index.test.tsx b/apps/web/modules/ui/components/settings-id/index.test.tsx
new file mode 100644
index 0000000000..4fdfc9e0c5
--- /dev/null
+++ b/apps/web/modules/ui/components/settings-id/index.test.tsx
@@ -0,0 +1,35 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { SettingsId } from "./index";
+
+describe("SettingsId", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders the title and id correctly", () => {
+ render( );
+
+ const element = screen.getByText(/Survey ID: survey-123/);
+ expect(element).toBeInTheDocument();
+ expect(element.tagName.toLowerCase()).toBe("p");
+ });
+
+ test("applies correct styling", () => {
+ render( );
+
+ const element = screen.getByText(/Environment ID: env-456/);
+ expect(element).toHaveClass("py-1");
+ expect(element).toHaveClass("text-xs");
+ expect(element).toHaveClass("text-slate-400");
+ });
+
+ test("renders with very long id", () => {
+ const longId = "a".repeat(100);
+ render( );
+
+ const element = screen.getByText(`API Key: ${longId}`);
+ expect(element).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/shuffle-option-select/index.test.tsx b/apps/web/modules/ui/components/shuffle-option-select/index.test.tsx
new file mode 100644
index 0000000000..3c6dc1207b
--- /dev/null
+++ b/apps/web/modules/ui/components/shuffle-option-select/index.test.tsx
@@ -0,0 +1,104 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TShuffleOption } from "@formbricks/types/surveys/types";
+import { ShuffleOptionSelect } from "./index";
+
+// Mock Select component
+vi.mock("@/modules/ui/components/select", () => ({
+ Select: ({ children, onValueChange, value }: any) => (
+
+
document.dispatchEvent(new Event("open-select"))}>
+ Open Select
+
+
{children}
+
+ ),
+ SelectContent: ({ children }: any) => {children}
,
+ SelectItem: ({ children, value }: any) => (
+ document.dispatchEvent(new CustomEvent("select-item", { detail: value }))}>
+ {children}
+
+ ),
+ SelectTrigger: ({ children }: any) => {children}
,
+ SelectValue: ({ placeholder }: any) => {placeholder}
,
+}));
+
+// Mock tolgee
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => (key === "environments.surveys.edit.select_ordering" ? "Select ordering" : key),
+ }),
+}));
+
+describe("ShuffleOptionSelect", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const shuffleOptionsTypes = {
+ none: { id: "none", label: "Don't shuffle", show: true },
+ all: { id: "all", label: "Shuffle all options", show: true },
+ exceptLast: { id: "exceptLast", label: "Shuffle all except last option", show: true },
+ };
+
+ const mockUpdateQuestion = vi.fn();
+
+ test("renders with default value", () => {
+ render(
+
+ );
+
+ expect(screen.getByTestId("select")).toBeInTheDocument();
+ expect(screen.getByTestId("select")).toHaveAttribute("data-value", "none");
+ expect(screen.getByTestId("select-value")).toHaveTextContent("Select ordering");
+ });
+
+ test("renders all shuffle options", () => {
+ render(
+
+ );
+
+ const selectItems = screen.getAllByTestId("select-item");
+ expect(selectItems).toHaveLength(3);
+ expect(selectItems[0]).toHaveTextContent("Don't shuffle");
+ expect(selectItems[1]).toHaveTextContent("Shuffle all options");
+ expect(selectItems[2]).toHaveTextContent("Shuffle all except last option");
+ });
+
+ test("only renders visible shuffle options", () => {
+ const limitedOptions = {
+ none: { id: "none", label: "Don't shuffle", show: true },
+ all: { id: "all", label: "Shuffle all options", show: false }, // This one shouldn't show
+ exceptLast: { id: "exceptLast", label: "Shuffle all except last option", show: true },
+ };
+
+ render(
+
+ );
+
+ const selectItems = screen.getAllByTestId("select-item");
+ expect(selectItems).toHaveLength(2);
+ expect(selectItems[0]).toHaveTextContent("Don't shuffle");
+ expect(selectItems[1]).toHaveTextContent("Shuffle all except last option");
+ });
+});
diff --git a/apps/web/modules/ui/components/skeleton-loader/index.test.tsx b/apps/web/modules/ui/components/skeleton-loader/index.test.tsx
new file mode 100644
index 0000000000..65a6634736
--- /dev/null
+++ b/apps/web/modules/ui/components/skeleton-loader/index.test.tsx
@@ -0,0 +1,73 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { SkeletonLoader } from "./index";
+
+// Mock the Skeleton component
+vi.mock("@/modules/ui/components/skeleton", () => ({
+ Skeleton: ({ className, children }: { className: string; children: React.ReactNode }) => (
+
+ {children}
+
+ ),
+}));
+
+describe("SkeletonLoader", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders summary skeleton loader correctly", () => {
+ render( );
+
+ expect(screen.getByTestId("skeleton-loader-summary")).toBeInTheDocument();
+ expect(screen.getByTestId("mocked-skeleton")).toHaveClass("group");
+ expect(screen.getByTestId("mocked-skeleton")).toHaveClass("space-y-4");
+ expect(screen.getByTestId("mocked-skeleton")).toHaveClass("rounded-xl");
+ expect(screen.getByTestId("mocked-skeleton")).toHaveClass("bg-white");
+ expect(screen.getByTestId("mocked-skeleton")).toHaveClass("p-6");
+
+ // Check for skeleton elements inside
+ const skeletonElements = document.querySelectorAll(".bg-slate-200");
+ expect(skeletonElements.length).toBeGreaterThan(0);
+ });
+
+ test("renders response skeleton loader correctly", () => {
+ render( );
+
+ expect(screen.getByTestId("skeleton-loader-response")).toBeInTheDocument();
+ expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("group");
+ expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("space-y-4");
+ expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("rounded-lg");
+ expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("bg-white");
+ expect(screen.getByTestId("skeleton-loader-response")).toHaveClass("p-6");
+
+ // Check for skeleton elements inside
+ const skeletonElements = document.querySelectorAll(".bg-slate-200");
+ expect(skeletonElements.length).toBeGreaterThan(0);
+
+ // Check for profile skeleton
+ const profileSkeleton = document.querySelector(".h-12.w-12.flex-shrink-0.rounded-full");
+ expect(profileSkeleton).toBeInTheDocument();
+ });
+
+ test("renders different structures for summary and response types", () => {
+ const { rerender } = render( );
+
+ const summaryContainer = screen.getByTestId("skeleton-loader-summary");
+ expect(summaryContainer).toBeInTheDocument();
+ expect(summaryContainer).toHaveClass("rounded-xl");
+ expect(summaryContainer).toHaveClass("border-slate-200");
+
+ // Rerender with response type
+ rerender( );
+
+ expect(screen.queryByTestId("skeleton-loader-summary")).not.toBeInTheDocument();
+ expect(screen.getByTestId("skeleton-loader-response")).toBeInTheDocument();
+
+ // Response type has no border class
+ const responseContainer = screen.getByTestId("skeleton-loader-response");
+ expect(responseContainer).not.toHaveClass("border");
+ expect(responseContainer).not.toHaveClass("border-slate-200");
+ });
+});
diff --git a/apps/web/modules/ui/components/skeleton/index.test.tsx b/apps/web/modules/ui/components/skeleton/index.test.tsx
new file mode 100644
index 0000000000..14bde3cd73
--- /dev/null
+++ b/apps/web/modules/ui/components/skeleton/index.test.tsx
@@ -0,0 +1,40 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { Skeleton } from "./index";
+
+describe("Skeleton", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders with default styling", () => {
+ const { container } = render( );
+ const skeletonElement = container.firstChild as HTMLElement;
+
+ expect(skeletonElement).toBeInTheDocument();
+ expect(skeletonElement).toHaveClass("animate-pulse");
+ expect(skeletonElement).toHaveClass("rounded-full");
+ expect(skeletonElement).toHaveClass("bg-slate-200");
+ });
+
+ test("passes additional props", () => {
+ const { container } = render( );
+ const skeletonElement = container.firstChild as HTMLElement;
+
+ expect(skeletonElement).toHaveAttribute("data-testid", "test-skeleton");
+ expect(skeletonElement).toHaveAttribute("aria-label", "Loading");
+ });
+
+ test("renders with children", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ const skeletonElement = container.firstChild as HTMLElement;
+ expect(skeletonElement).toBeInTheDocument();
+ expect(skeletonElement.textContent).toBe("Content");
+ });
+});
diff --git a/apps/web/modules/ui/components/slider/index.test.tsx b/apps/web/modules/ui/components/slider/index.test.tsx
new file mode 100644
index 0000000000..163ab8b37c
--- /dev/null
+++ b/apps/web/modules/ui/components/slider/index.test.tsx
@@ -0,0 +1,97 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { Slider } from "./index";
+
+// Mock Radix UI Slider components
+vi.mock("@radix-ui/react-slider", () => ({
+ Root: ({ className, defaultValue, value, onValueChange, disabled, ...props }: any) => (
+ {
+ if (!disabled && onValueChange) {
+ // Simulate slider change on click (simplified for testing)
+ const newValue = value ? [value[0] + 10] : [50];
+ onValueChange(newValue);
+ }
+ }}
+ {...props}
+ />
+ ),
+ Track: ({ className, children }: any) => (
+
+ {children}
+
+ ),
+ Range: ({ className }: any) =>
,
+ Thumb: ({ className }: any) =>
,
+}));
+
+describe("Slider", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders with default props", () => {
+ render(
);
+
+ expect(screen.getByTestId("slider-root")).toBeInTheDocument();
+ expect(screen.getByTestId("slider-track")).toBeInTheDocument();
+ expect(screen.getByTestId("slider-range")).toBeInTheDocument();
+ expect(screen.getByTestId("slider-thumb")).toBeInTheDocument();
+ });
+
+ test("applies custom className", () => {
+ render(
);
+
+ const sliderRoot = screen.getByTestId("slider-root");
+ expect(sliderRoot).toHaveClass("custom-class");
+ expect(sliderRoot).toHaveClass("relative");
+ expect(sliderRoot).toHaveClass("flex");
+ expect(sliderRoot).toHaveClass("w-full");
+ });
+
+ test("accepts defaultValue prop", () => {
+ render(
);
+
+ const sliderRoot = screen.getByTestId("slider-root");
+ expect(sliderRoot).toHaveAttribute("data-value", "25");
+ });
+
+ test("handles value changes", async () => {
+ const handleValueChange = vi.fn();
+ const user = userEvent.setup();
+
+ render(
);
+
+ const sliderRoot = screen.getByTestId("slider-root");
+ expect(sliderRoot).toHaveAttribute("data-value", "30");
+
+ await user.click(sliderRoot);
+
+ expect(handleValueChange).toHaveBeenCalledWith([40]);
+ });
+
+ test("renders in disabled state", () => {
+ render(
);
+
+ const sliderRoot = screen.getByTestId("slider-root");
+ expect(sliderRoot).toHaveAttribute("data-disabled", "true");
+ });
+
+ test("doesn't call onValueChange when disabled", async () => {
+ const handleValueChange = vi.fn();
+ const user = userEvent.setup();
+
+ render(
);
+
+ const sliderRoot = screen.getByTestId("slider-root");
+ await user.click(sliderRoot);
+
+ expect(handleValueChange).not.toHaveBeenCalled();
+ });
+});
diff --git a/apps/web/modules/ui/components/stacked-cards-container/index.test.tsx b/apps/web/modules/ui/components/stacked-cards-container/index.test.tsx
new file mode 100644
index 0000000000..c2b0a960c9
--- /dev/null
+++ b/apps/web/modules/ui/components/stacked-cards-container/index.test.tsx
@@ -0,0 +1,106 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { StackedCardsContainer } from "./index";
+
+describe("StackedCardsContainer", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders children correctly", () => {
+ render(
+
+ Test Content
+
+ );
+
+ expect(screen.getByTestId("test-child")).toBeInTheDocument();
+ expect(screen.getByText("Test Content")).toBeInTheDocument();
+ });
+
+ test("renders with 'simple' arrangement", () => {
+ const { container } = render(
+
+ Test Content
+
+ );
+
+ // Should have only one div with specific classes for "none" layout
+ const mainContainer = container.firstChild as HTMLElement;
+ expect(mainContainer).toHaveClass("flex");
+ expect(mainContainer).toHaveClass("flex-col");
+ expect(mainContainer).toHaveClass("items-center");
+ expect(mainContainer).toHaveClass("justify-center");
+ expect(mainContainer).toHaveClass("rounded-xl");
+ expect(mainContainer).toHaveClass("border");
+ expect(mainContainer).toHaveClass("border-slate-200");
+
+ // Should not have shadow cards
+ const allDivs = container.querySelectorAll("div");
+ expect(allDivs.length).toBe(2); // Main container + child div
+ });
+
+ test("renders with 'casual' arrangement", () => {
+ const { container } = render(
+
+ Test Content
+
+ );
+
+ // Should have a group container
+ const groupContainer = container.firstChild as HTMLElement;
+ expect(groupContainer).toHaveClass("group");
+ expect(groupContainer).toHaveClass("relative");
+
+ // Should have shadow cards
+ const allDivs = container.querySelectorAll("div");
+ expect(allDivs.length).toBe(5); // Group + 2 shadow cards + content container + child div
+
+ // Check for shadow cards with rotation
+ const shadowCards = container.querySelectorAll(".absolute");
+ expect(shadowCards.length).toBe(2);
+ expect(shadowCards[0]).toHaveClass("-rotate-6");
+ expect(shadowCards[1]).toHaveClass("-rotate-3");
+ });
+
+ test("renders with 'straight' arrangement", () => {
+ const { container } = render(
+
+ Test Content
+
+ );
+
+ // Should have a group container
+ const groupContainer = container.firstChild as HTMLElement;
+ expect(groupContainer).toHaveClass("group");
+ expect(groupContainer).toHaveClass("relative");
+
+ // Should have shadow cards
+ const allDivs = container.querySelectorAll("div");
+ expect(allDivs.length).toBe(5); // Group + 2 shadow cards + content container + child div
+
+ // Check for shadow cards with translation
+ const shadowCards = container.querySelectorAll(".absolute");
+ expect(shadowCards.length).toBe(2);
+ expect(shadowCards[0]).toHaveClass("-translate-y-8");
+ expect(shadowCards[1]).toHaveClass("-translate-y-4");
+ });
+
+ test("falls back to 'simple' arrangement for unknown type", () => {
+ // @ts-ignore - Testing with invalid input
+ const { container } = render(
+
+ Test Content
+
+ );
+
+ // Should have the same structure as "none"
+ const mainContainer = container.firstChild as HTMLElement;
+ expect(mainContainer).toHaveClass("flex");
+ expect(mainContainer).toHaveClass("flex-col");
+
+ const allDivs = container.querySelectorAll("div");
+ expect(allDivs.length).toBe(2); // Main container + child div
+ });
+});
diff --git a/apps/web/modules/ui/components/styling-tabs/index.test.tsx b/apps/web/modules/ui/components/styling-tabs/index.test.tsx
new file mode 100644
index 0000000000..4f4aa6214d
--- /dev/null
+++ b/apps/web/modules/ui/components/styling-tabs/index.test.tsx
@@ -0,0 +1,109 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { StylingTabs } from "./index";
+
+describe("StylingTabs", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockOptions = [
+ { value: "option1", label: "Option 1" },
+ { value: "option2", label: "Option 2" },
+ { value: "option3", label: "Option 3" },
+ ];
+
+ test("renders with all options", () => {
+ render(
{}} />);
+
+ expect(screen.getByText("Option 1")).toBeInTheDocument();
+ expect(screen.getByText("Option 2")).toBeInTheDocument();
+ expect(screen.getByText("Option 3")).toBeInTheDocument();
+ });
+
+ test("selects default option when provided", () => {
+ render(
+ {}} />
+ );
+
+ const option2Input = screen.getByLabelText("Option 2");
+ expect(option2Input).toBeChecked();
+ });
+
+ test("calls onChange handler when option is selected", async () => {
+ const handleChange = vi.fn();
+ const user = userEvent.setup();
+
+ render( );
+
+ await user.click(screen.getByText("Option 3"));
+
+ expect(handleChange).toHaveBeenCalledWith("option3");
+ });
+
+ test("renders with label and subLabel", () => {
+ render(
+ {}}
+ label="Test Label"
+ subLabel="Test Sublabel"
+ />
+ );
+
+ expect(screen.getByText("Test Label")).toBeInTheDocument();
+ expect(screen.getByText("Test Sublabel")).toBeInTheDocument();
+ });
+
+ test("renders with custom className", () => {
+ const { container } = render(
+ {}} className="custom-class" />
+ );
+
+ const radioGroup = container.querySelector('[role="radiogroup"]');
+ expect(radioGroup).toHaveClass("custom-class");
+ });
+
+ test("renders with custom tabsContainerClassName", () => {
+ const { container } = render(
+ {}}
+ tabsContainerClassName="custom-tabs-class"
+ />
+ );
+
+ const tabsContainer = container.querySelector(".overflow-hidden.rounded-md.border");
+ expect(tabsContainer).toHaveClass("custom-tabs-class");
+ });
+
+ test("renders options with icons when provided", () => {
+ const optionsWithIcons = [
+ { value: "option1", label: "Option 1", icon: Icon 1 },
+ { value: "option2", label: "Option 2", icon: Icon 2 },
+ ];
+
+ render( {}} />);
+
+ expect(screen.getByTestId("icon1")).toBeInTheDocument();
+ expect(screen.getByTestId("icon2")).toBeInTheDocument();
+ });
+
+ test("applies selected styling to active option", async () => {
+ const user = userEvent.setup();
+
+ render( {}} />);
+
+ const option1Label = screen.getByText("Option 1").closest("label");
+ const option2Label = screen.getByText("Option 2").closest("label");
+
+ await user.click(screen.getByText("Option 2"));
+
+ expect(option1Label).not.toHaveClass("bg-slate-100");
+ expect(option2Label).toHaveClass("bg-slate-100");
+ });
+});
diff --git a/apps/web/modules/ui/components/survey-status-indicator/index.test.tsx b/apps/web/modules/ui/components/survey-status-indicator/index.test.tsx
new file mode 100644
index 0000000000..4ce385f8fd
--- /dev/null
+++ b/apps/web/modules/ui/components/survey-status-indicator/index.test.tsx
@@ -0,0 +1,169 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { SurveyStatusIndicator } from "./index";
+
+// Mock the tooltip component
+vi.mock("@/modules/ui/components/tooltip", () => ({
+ Tooltip: ({ children }: { children: React.ReactNode }) => {children}
,
+ TooltipContent: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ TooltipProvider: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ TooltipTrigger: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
+// Mock the lucide-react icons
+vi.mock("lucide-react", () => ({
+ CheckIcon: () =>
,
+ ClockIcon: () =>
,
+ PauseIcon: () =>
,
+ PencilIcon: () =>
,
+}));
+
+// Mock tolgee
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ "common.gathering_responses": "Gathering responses",
+ "common.survey_scheduled": "Survey scheduled",
+ "common.survey_paused": "Survey paused",
+ "common.survey_completed": "Survey completed",
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
+
+describe("SurveyStatusIndicator", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders inProgress status correctly without tooltip", () => {
+ const { container } = render( );
+
+ // Find the green dot using container query instead of getByText
+ const greenDotContainer = container.querySelector(".relative.flex.h-3.w-3");
+ expect(greenDotContainer).toBeInTheDocument();
+
+ // Check the children elements
+ const pingElement = greenDotContainer?.querySelector(".animate-ping-slow");
+ const dotElement = greenDotContainer?.querySelector(".relative.inline-flex");
+
+ expect(pingElement).toHaveClass("bg-green-500");
+ expect(dotElement).toHaveClass("bg-green-500");
+
+ // Should not render tooltip components
+ expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
+ });
+
+ test("renders scheduled status correctly without tooltip", () => {
+ const { container } = render( );
+
+ // Find the clock icon container
+ const clockIconContainer = container.querySelector(".rounded-full.bg-slate-300.p-1");
+ expect(clockIconContainer).toBeInTheDocument();
+
+ // Find the clock icon inside
+ const clockIcon = clockIconContainer?.querySelector("[data-testid='clock-icon']");
+ expect(clockIcon).toBeInTheDocument();
+
+ // Should not render tooltip components
+ expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
+ });
+
+ test("renders paused status correctly without tooltip", () => {
+ const { container } = render( );
+
+ // Find the pause icon container
+ const pauseIconContainer = container.querySelector(".rounded-full.bg-slate-300.p-1");
+ expect(pauseIconContainer).toBeInTheDocument();
+
+ // Find the pause icon inside
+ const pauseIcon = pauseIconContainer?.querySelector("[data-testid='pause-icon']");
+ expect(pauseIcon).toBeInTheDocument();
+
+ // Should not render tooltip components
+ expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
+ });
+
+ test("renders completed status correctly without tooltip", () => {
+ const { container } = render( );
+
+ // Find the check icon container
+ const checkIconContainer = container.querySelector(".rounded-full.bg-slate-200.p-1");
+ expect(checkIconContainer).toBeInTheDocument();
+
+ // Find the check icon inside
+ const checkIcon = checkIconContainer?.querySelector("[data-testid='check-icon']");
+ expect(checkIcon).toBeInTheDocument();
+
+ // Should not render tooltip components
+ expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
+ });
+
+ test("renders draft status correctly without tooltip", () => {
+ const { container } = render( );
+
+ // Find the pencil icon container
+ const pencilIconContainer = container.querySelector(".rounded-full.bg-slate-300.p-1");
+ expect(pencilIconContainer).toBeInTheDocument();
+
+ // Find the pencil icon inside
+ const pencilIcon = pencilIconContainer?.querySelector("[data-testid='pencil-icon']");
+ expect(pencilIcon).toBeInTheDocument();
+
+ // Should not render tooltip components
+ expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
+ });
+
+ test("renders with tooltip when tooltip prop is true", () => {
+ render( );
+
+ // Should render tooltip components
+ expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip-content")).toBeInTheDocument();
+
+ // Should have the right content in the tooltip
+ const tooltipContent = screen.getByTestId("tooltip-content");
+ expect(tooltipContent).toHaveTextContent("Gathering responses");
+ });
+
+ test("renders scheduled status with tooltip correctly", () => {
+ const { container } = render( );
+
+ expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Survey scheduled");
+
+ // Use container query to find the first clock icon
+ const clockIcon = container.querySelector("[data-testid='clock-icon']");
+ expect(clockIcon).toBeInTheDocument();
+ });
+
+ test("renders paused status with tooltip correctly", () => {
+ const { container } = render( );
+
+ expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Survey paused");
+
+ // Use container query to find the first pause icon
+ const pauseIcon = container.querySelector("[data-testid='pause-icon']");
+ expect(pauseIcon).toBeInTheDocument();
+ });
+
+ test("renders completed status with tooltip correctly", () => {
+ const { container } = render( );
+
+ expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Survey completed");
+
+ // Use container query to find the first check icon
+ const checkIcon = container.querySelector("[data-testid='check-icon']");
+ expect(checkIcon).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/survey/index.test.tsx b/apps/web/modules/ui/components/survey/index.test.tsx
new file mode 100644
index 0000000000..e8c3e7512c
--- /dev/null
+++ b/apps/web/modules/ui/components/survey/index.test.tsx
@@ -0,0 +1,171 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import { SurveyInline } from "./index";
+import * as recaptchaModule from "./recaptcha";
+
+// Mock survey loading functionality
+vi.mock("@/modules/ui/components/survey/recaptcha", () => ({
+ loadRecaptchaScript: vi.fn().mockResolvedValue(undefined),
+ executeRecaptcha: vi.fn().mockResolvedValue("mock-recaptcha-token"),
+}));
+
+describe("SurveyInline", () => {
+ const mockRenderSurvey = vi.fn();
+
+ beforeEach(() => {
+ // Mock fetch to prevent actual network requests
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ text: () => Promise.resolve("console.log('Survey script loaded');"),
+ } as Response);
+
+ // Setup window.formbricksSurveys
+ window.formbricksSurveys = {
+ renderSurveyInline: vi.fn(),
+ renderSurveyModal: vi.fn(),
+ renderSurvey: mockRenderSurvey,
+ onFilePick: vi.fn(),
+ };
+
+ // Mock script loading functionality
+ Object.defineProperty(window, "formbricksSurveys", {
+ value: {
+ renderSurveyInline: vi.fn(),
+ renderSurveyModal: vi.fn(),
+ renderSurvey: mockRenderSurvey,
+ onFilePick: vi.fn(),
+ },
+ writable: true,
+ });
+
+ // Mock the document.createElement and appendChild methods
+ // to avoid actual DOM manipulation in tests
+ const originalCreateElement = document.createElement;
+
+ vi.spyOn(document, "createElement").mockImplementation((tagName) => {
+ if (tagName === "script") {
+ const mockScript = originalCreateElement.call(document, "script");
+ Object.defineProperty(mockScript, "textContent", {
+ set: () => {
+ /* mock setter */
+ },
+ get: () => "",
+ });
+ return mockScript;
+ }
+ return originalCreateElement.call(document, tagName);
+ });
+
+ vi.spyOn(document.head, "appendChild").mockImplementation(() => document.head);
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ // @ts-ignore
+ delete window.formbricksSurveys;
+ });
+
+ test("renders a container with the correct ID", () => {
+ const { container } = render(
+
+ );
+
+ const surveyContainer = container.querySelector('[id^="formbricks-survey-container"]');
+ expect(surveyContainer).toBeInTheDocument();
+ expect(surveyContainer).toHaveClass("h-full");
+ expect(surveyContainer).toHaveClass("w-full");
+ });
+
+ test("calls renderSurvey with correct props when formbricksSurveys is available", async () => {
+ const mockSurvey = { id: "survey1" };
+
+ render(
+
+ );
+
+ // Verify the mock was called with correct props
+ expect(mockRenderSurvey).toHaveBeenCalled();
+
+ const callArgs = mockRenderSurvey.mock.calls[0][0];
+ expect(callArgs.survey).toBe(mockSurvey);
+ expect(callArgs.mode).toBe("inline");
+ expect(callArgs.containerId).toMatch(/formbricks-survey-container/);
+ });
+
+ test("doesn't load recaptcha script when isSpamProtectionEnabled is false", async () => {
+ const loadRecaptchaScriptMock = vi.mocked(recaptchaModule.loadRecaptchaScript);
+ loadRecaptchaScriptMock.mockClear(); // Reset mock call counts
+
+ render(
+
+ );
+
+ expect(loadRecaptchaScriptMock).not.toHaveBeenCalled();
+ });
+
+ test("handles script loading error gracefully", async () => {
+ // Remove formbricksSurveys to test script loading
+ // @ts-ignore
+ delete window.formbricksSurveys;
+
+ // Mock fetch to reject
+ vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Failed to load script"));
+
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ render(
+
+ );
+
+ // Wait for the error to be logged
+ await vi.waitFor(() => {
+ expect(consoleSpy).toHaveBeenCalledWith("Failed to load the surveys package: ", expect.any(Error));
+ });
+
+ consoleSpy.mockRestore();
+ });
+
+ test("provides a getRecaptchaToken function to the survey renderer", async () => {
+ const executeRecaptchaMock = vi.mocked(recaptchaModule.executeRecaptcha);
+ executeRecaptchaMock.mockClear(); // Reset mock call counts
+
+ render(
+
+ );
+
+ // Verify the mock was called with the right function
+ expect(mockRenderSurvey).toHaveBeenCalled();
+
+ // Get the getRecaptchaToken function from the props
+ const callArgs = mockRenderSurvey.mock.calls[0][0];
+ expect(callArgs.getRecaptchaToken).toBeDefined();
+
+ // Call the function to verify it works
+ await callArgs.getRecaptchaToken();
+ expect(executeRecaptchaMock).toHaveBeenCalledWith("test-site-key");
+ });
+});
diff --git a/apps/web/modules/ui/components/switch/index.test.tsx b/apps/web/modules/ui/components/switch/index.test.tsx
new file mode 100644
index 0000000000..77c3d1d488
--- /dev/null
+++ b/apps/web/modules/ui/components/switch/index.test.tsx
@@ -0,0 +1,112 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { Switch } from "./index";
+
+// Mock radix-ui components
+vi.mock("@radix-ui/react-switch", () => ({
+ Root: ({ className, checked, onCheckedChange, disabled, id, "aria-label": ariaLabel }: any) => (
+ !disabled && onCheckedChange && onCheckedChange(!checked)}
+ disabled={disabled}
+ id={id}
+ aria-label={ariaLabel}>
+
+
+ ),
+ Thumb: ({ className, checked }: any) => (
+
+ ),
+}));
+
+describe("Switch", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders default switch correctly", () => {
+ render( );
+
+ const switchRoot = screen.getByTestId("switch-root");
+ expect(switchRoot).toBeInTheDocument();
+
+ // Check default state classes
+ expect(switchRoot).toHaveClass("peer");
+ expect(switchRoot).toHaveClass("inline-flex");
+ expect(switchRoot).toHaveClass("rounded-full");
+ expect(switchRoot).toHaveClass("border-2");
+
+ // Check default state (unchecked)
+ expect(switchRoot).toHaveAttribute("data-state", "unchecked");
+
+ // Check thumb element
+ const switchThumb = screen.getByTestId("switch-thumb");
+ expect(switchThumb).toBeInTheDocument();
+ expect(switchThumb).toHaveAttribute("data-state", "unchecked");
+ });
+
+ test("applies custom className", () => {
+ render( );
+
+ const switchRoot = screen.getByTestId("switch-root");
+ expect(switchRoot).toHaveClass("custom-class");
+ });
+
+ test("renders in checked state", () => {
+ render( );
+
+ const switchRoot = screen.getByTestId("switch-root");
+ expect(switchRoot).toHaveAttribute("data-state", "checked");
+
+ const switchThumb = screen.getByTestId("switch-thumb");
+ expect(switchThumb).toHaveAttribute("data-state", "checked");
+ });
+
+ test("renders in disabled state", () => {
+ render( );
+
+ const switchRoot = screen.getByTestId("switch-root");
+ expect(switchRoot).toBeDisabled();
+ });
+
+ test("handles onChange callback", async () => {
+ const handleChange = vi.fn();
+ const user = userEvent.setup();
+
+ render( );
+
+ const switchRoot = screen.getByTestId("switch-root");
+ await user.click(switchRoot);
+
+ expect(handleChange).toHaveBeenCalledTimes(1);
+ expect(handleChange).toHaveBeenCalledWith(true);
+ });
+
+ test("doesn't trigger onChange when disabled", async () => {
+ const handleChange = vi.fn();
+ const user = userEvent.setup();
+
+ render( );
+
+ const switchRoot = screen.getByTestId("switch-root");
+ await user.click(switchRoot);
+
+ expect(handleChange).not.toHaveBeenCalled();
+ });
+
+ test("passes props correctly", () => {
+ render( );
+
+ const switchRoot = screen.getByTestId("switch-root");
+ expect(switchRoot).toHaveAttribute("id", "test-switch");
+ expect(switchRoot).toHaveAttribute("aria-label", "Toggle");
+ });
+});
diff --git a/apps/web/modules/ui/components/tab-bar/index.test.tsx b/apps/web/modules/ui/components/tab-bar/index.test.tsx
new file mode 100644
index 0000000000..270dcca879
--- /dev/null
+++ b/apps/web/modules/ui/components/tab-bar/index.test.tsx
@@ -0,0 +1,97 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TabBar } from "./index";
+
+describe("TabBar", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockTabs = [
+ { id: "tab1", label: "Tab One" },
+ { id: "tab2", label: "Tab Two" },
+ { id: "tab3", label: "Tab Three" },
+ ];
+
+ test("calls setActiveId when tab is clicked", async () => {
+ const handleSetActiveId = vi.fn();
+ const user = userEvent.setup();
+
+ render( );
+
+ await user.click(screen.getByText("Tab Two"));
+
+ expect(handleSetActiveId).toHaveBeenCalledTimes(1);
+ expect(handleSetActiveId).toHaveBeenCalledWith("tab2");
+ });
+
+ test("renders tabs with icons", () => {
+ const tabsWithIcons = [
+ { id: "tab1", label: "Tab One", icon: 🔍 },
+ { id: "tab2", label: "Tab Two", icon: 📁 },
+ ];
+
+ render( {}} />);
+
+ expect(screen.getByTestId("icon1")).toBeInTheDocument();
+ expect(screen.getByTestId("icon2")).toBeInTheDocument();
+ });
+
+ test("applies custom className", () => {
+ const { container } = render(
+ {}} className="custom-class" />
+ );
+
+ const tabContainer = container.firstChild as HTMLElement;
+ expect(tabContainer).toHaveClass("custom-class");
+ });
+
+ test("applies activeTabClassName to active tab", () => {
+ render(
+ {}}
+ activeTabClassName="custom-active-class"
+ />
+ );
+
+ const activeTab = screen.getByText("Tab One").closest("button");
+ expect(activeTab).toHaveClass("custom-active-class");
+ });
+
+ test("renders in disabled state", async () => {
+ const handleSetActiveId = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ const navContainer = screen.getByRole("navigation");
+ expect(navContainer).toHaveClass("cursor-not-allowed");
+ expect(navContainer).toHaveClass("opacity-50");
+
+ await user.click(screen.getByText("Tab Two"));
+
+ expect(handleSetActiveId).not.toHaveBeenCalled();
+ });
+
+ test("doesn't apply disabled styles when not disabled", () => {
+ render(
+ {}} tabStyle="button" disabled={false} />
+ );
+
+ const navContainer = screen.getByRole("navigation");
+ expect(navContainer).not.toHaveClass("cursor-not-allowed");
+ expect(navContainer).not.toHaveClass("opacity-50");
+ });
+});
diff --git a/apps/web/modules/ui/components/tab-toggle/index.test.tsx b/apps/web/modules/ui/components/tab-toggle/index.test.tsx
new file mode 100644
index 0000000000..b3a28d307b
--- /dev/null
+++ b/apps/web/modules/ui/components/tab-toggle/index.test.tsx
@@ -0,0 +1,121 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TabToggle } from "./index";
+
+describe("TabToggle", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockOptions = [
+ { value: "option1", label: "Option 1" },
+ { value: "option2", label: "Option 2" },
+ { value: "option3", label: "Option 3" },
+ ];
+
+ test("renders all options correctly", () => {
+ render( {}} />);
+
+ expect(screen.getByLabelText("Option 1")).toBeInTheDocument();
+ expect(screen.getByLabelText("Option 2")).toBeInTheDocument();
+ expect(screen.getByLabelText("Option 3")).toBeInTheDocument();
+ });
+
+ test("selects default option when provided", () => {
+ render( {}} />);
+
+ const option1Radio = screen.getByLabelText("Option 1") as HTMLInputElement;
+ const option2Radio = screen.getByLabelText("Option 2") as HTMLInputElement;
+ const option3Radio = screen.getByLabelText("Option 3") as HTMLInputElement;
+
+ expect(option1Radio.checked).toBe(false);
+ expect(option2Radio.checked).toBe(true);
+ expect(option3Radio.checked).toBe(false);
+ });
+
+ test("calls onChange handler when option is selected", async () => {
+ const handleChange = vi.fn();
+ const user = userEvent.setup();
+
+ render( );
+
+ await user.click(screen.getByLabelText("Option 2"));
+
+ expect(handleChange).toHaveBeenCalledTimes(1);
+ expect(handleChange).toHaveBeenCalledWith("option2");
+ });
+
+ test("displays option labels correctly", () => {
+ render( {}} />);
+
+ expect(screen.getByText("Option 1")).toBeInTheDocument();
+ expect(screen.getByText("Option 2")).toBeInTheDocument();
+ expect(screen.getByText("Option 3")).toBeInTheDocument();
+ });
+
+ test("applies correct styling to selected option", async () => {
+ const user = userEvent.setup();
+
+ render( {}} />);
+
+ const option2Label = screen.getByText("Option 2").closest("label");
+ expect(option2Label).not.toHaveClass("bg-white");
+
+ await user.click(screen.getByLabelText("Option 2"));
+
+ expect(option2Label).toHaveClass("bg-white");
+ });
+
+ test("renders in disabled state", () => {
+ render( {}} disabled={true} />);
+
+ const option1Radio = screen.getByLabelText("Option 1") as HTMLInputElement;
+ const option2Radio = screen.getByLabelText("Option 2") as HTMLInputElement;
+ const option3Radio = screen.getByLabelText("Option 3") as HTMLInputElement;
+
+ expect(option1Radio).toBeDisabled();
+ expect(option2Radio).toBeDisabled();
+ expect(option3Radio).toBeDisabled();
+
+ const labels = screen.getAllByRole("radio").map((radio) => radio.closest("label"));
+ labels.forEach((label) => {
+ expect(label).toHaveClass("cursor-not-allowed");
+ expect(label).toHaveClass("opacity-50");
+ });
+ });
+
+ test("doesn't call onChange when disabled", async () => {
+ const handleChange = vi.fn();
+ const user = userEvent.setup();
+
+ render( );
+
+ await user.click(screen.getByLabelText("Option 2"));
+
+ expect(handleChange).not.toHaveBeenCalled();
+ });
+
+ test("renders with number values", () => {
+ const numberOptions = [
+ { value: 1, label: "One" },
+ { value: 2, label: "Two" },
+ ];
+
+ render( {}} />);
+
+ const option1Radio = screen.getByLabelText("One") as HTMLInputElement;
+ const option2Radio = screen.getByLabelText("Two") as HTMLInputElement;
+
+ expect(option1Radio.checked).toBe(true);
+ expect(option2Radio.checked).toBe(false);
+ });
+
+ test("sets correct aria attributes", () => {
+ render( {}} />);
+
+ const radioGroup = screen.getByRole("radiogroup");
+ expect(radioGroup).toHaveAttribute("aria-labelledby", "test-id-toggle-label");
+ });
+});
diff --git a/apps/web/modules/ui/components/table/index.test.tsx b/apps/web/modules/ui/components/table/index.test.tsx
new file mode 100644
index 0000000000..35c5e09396
--- /dev/null
+++ b/apps/web/modules/ui/components/table/index.test.tsx
@@ -0,0 +1,202 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import {
+ Table,
+ TableBody,
+ TableCaption,
+ TableCell,
+ TableFooter,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "./index";
+
+describe("Table", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders table correctly", () => {
+ render();
+
+ const table = screen.getByTestId("test-table");
+ expect(table).toBeInTheDocument();
+ expect(table.tagName).toBe("TABLE");
+ expect(table).toHaveClass("w-full");
+ expect(table).toHaveClass("caption-bottom");
+ expect(table).toHaveClass("text-sm");
+ });
+
+ test("applies custom className to Table", () => {
+ render();
+
+ const table = screen.getByTestId("test-table");
+ expect(table).toHaveClass("custom-class");
+ expect(table).toHaveClass("w-full");
+ });
+
+ test("renders TableHeader correctly", () => {
+ render(
+
+ );
+
+ const header = screen.getByTestId("test-header");
+ expect(header).toBeInTheDocument();
+ expect(header.tagName).toBe("THEAD");
+ expect(header).toHaveClass("pointer-events-none");
+ expect(header).toHaveClass("text-slate-800");
+ });
+
+ test("renders TableBody correctly", () => {
+ render(
+
+ );
+
+ const body = screen.getByTestId("test-body");
+ expect(body).toBeInTheDocument();
+ expect(body.tagName).toBe("TBODY");
+ });
+
+ test("renders TableFooter correctly", () => {
+ render(
+
+ );
+
+ const footer = screen.getByTestId("test-footer");
+ expect(footer).toBeInTheDocument();
+ expect(footer.tagName).toBe("TFOOT");
+ expect(footer).toHaveClass("border-t");
+ });
+
+ test("renders TableRow correctly", () => {
+ render(
+
+ );
+
+ const row = screen.getByTestId("test-row");
+ expect(row).toBeInTheDocument();
+ expect(row.tagName).toBe("TR");
+ expect(row).toHaveClass("border-b");
+ expect(row).toHaveClass("bg-white");
+ expect(row).toHaveClass("hover:bg-slate-100");
+ });
+
+ test("renders TableHead correctly", () => {
+ render(
+
+ );
+
+ const head = screen.getByTestId("test-head");
+ expect(head).toBeInTheDocument();
+ expect(head.tagName).toBe("TH");
+ expect(head).toHaveClass("h-12");
+ expect(head).toHaveClass("px-4");
+ expect(head).toHaveClass("text-left");
+ expect(head).toHaveClass("align-middle");
+ });
+
+ test("renders TableCell correctly", () => {
+ render(
+
+ );
+
+ const cell = screen.getByTestId("test-cell");
+ expect(cell).toBeInTheDocument();
+ expect(cell.tagName).toBe("TD");
+ expect(cell).toHaveClass("p-4");
+ expect(cell).toHaveClass("align-middle");
+ });
+
+ test("renders TableCaption correctly", () => {
+ render(
+
+ );
+
+ const caption = screen.getByTestId("test-caption");
+ expect(caption).toBeInTheDocument();
+ expect(caption.tagName).toBe("CAPTION");
+ expect(caption).toHaveClass("mt-4");
+ expect(caption).toHaveClass("text-sm");
+ expect(caption.textContent).toBe("Caption");
+ });
+
+ test("renders full table structure correctly", () => {
+ render(
+
+ A list of users
+
+
+ Name
+ Email
+
+
+
+
+ John Doe
+ john@example.com
+
+
+ Jane Smith
+ jane@example.com
+
+
+
+
+ Total: 2 users
+
+
+
+ );
+
+ const table = screen.getByTestId("full-table");
+ expect(table).toBeInTheDocument();
+
+ expect(screen.getByText("A list of users")).toBeInTheDocument();
+ expect(screen.getByText("Name")).toBeInTheDocument();
+ expect(screen.getByText("Email")).toBeInTheDocument();
+ expect(screen.getByText("John Doe")).toBeInTheDocument();
+ expect(screen.getByText("john@example.com")).toBeInTheDocument();
+ expect(screen.getByText("Jane Smith")).toBeInTheDocument();
+ expect(screen.getByText("jane@example.com")).toBeInTheDocument();
+ expect(screen.getByText("Total: 2 users")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/tag/index.test.tsx b/apps/web/modules/ui/components/tag/index.test.tsx
new file mode 100644
index 0000000000..0fcb4d4fb3
--- /dev/null
+++ b/apps/web/modules/ui/components/tag/index.test.tsx
@@ -0,0 +1,40 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { Tag } from "./index";
+
+describe("Tag", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders tag with correct name", () => {
+ render( {}} />);
+
+ expect(screen.getByText("Test Tag")).toBeInTheDocument();
+ });
+
+ test("applies highlight class when highlight prop is true", () => {
+ const { container } = render(
+ {}} highlight={true} />
+ );
+
+ const tagElement = container.firstChild as HTMLElement;
+ expect(tagElement).toHaveClass("animate-shake");
+ });
+
+ test("does not apply highlight class when highlight prop is false", () => {
+ const { container } = render(
+ {}} highlight={false} />
+ );
+
+ const tagElement = container.firstChild as HTMLElement;
+ expect(tagElement).not.toHaveClass("animate-shake");
+ });
+
+ test("does not render delete icon when allowDelete is false", () => {
+ render( {}} allowDelete={false} />);
+
+ expect(screen.queryByRole("img", { hidden: true })).not.toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/tags-combobox/index.test.tsx b/apps/web/modules/ui/components/tags-combobox/index.test.tsx
new file mode 100644
index 0000000000..91261e7dd4
--- /dev/null
+++ b/apps/web/modules/ui/components/tags-combobox/index.test.tsx
@@ -0,0 +1,184 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TagsCombobox } from "./index";
+
+// Mock components
+vi.mock("@/modules/ui/components/button", () => ({
+ Button: ({ children, onClick, size }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("@/modules/ui/components/command", () => ({
+ Command: ({ children, filter }: any) => (
+
+ {children}
+
+ ),
+ CommandGroup: ({ children }: any) => {children}
,
+ CommandInput: ({ placeholder, value, onValueChange, onKeyDown }: any) => (
+ onValueChange(e.target.value)}
+ onKeyDown={onKeyDown}
+ />
+ ),
+ CommandItem: ({ children, value, onSelect, className }: any) => (
+ onSelect(value)} className={className}>
+ {children}
+
+ ),
+ CommandList: ({ children }: any) => {children}
,
+}));
+
+vi.mock("@/modules/ui/components/popover", () => ({
+ Popover: ({ children, open }: any) => (
+
+ {children}
+
+ ),
+ PopoverContent: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+ PopoverTrigger: ({ children, asChild }: any) => (
+
+ {children}
+
+ ),
+}));
+
+// Mock tolgee
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ "environments.project.tags.add_tag": "Add tag",
+ "environments.project.tags.search_tags": "Search tags",
+ "environments.project.tags.add": "Add",
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
+
+describe("TagsCombobox", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockTags = [
+ { label: "Tag1", value: "tag1" },
+ { label: "Tag2", value: "tag2" },
+ { label: "Tag3", value: "tag3" },
+ ];
+
+ const mockCurrentTags = [{ label: "Tag1", value: "tag1" }];
+
+ const mockProps = {
+ tags: mockTags,
+ currentTags: mockCurrentTags,
+ addTag: vi.fn(),
+ createTag: vi.fn(),
+ searchValue: "",
+ setSearchValue: vi.fn(),
+ open: false,
+ setOpen: vi.fn(),
+ };
+
+ test("renders with default props", () => {
+ render( );
+
+ expect(screen.getByTestId("popover")).toBeInTheDocument();
+ expect(screen.getByTestId("popover-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("button")).toBeInTheDocument();
+ expect(screen.getByTestId("button")).toHaveTextContent("Add tag");
+ });
+
+ test("renders popover content when open is true", () => {
+ render( );
+
+ expect(screen.getByTestId("popover")).toHaveAttribute("data-open", "true");
+ expect(screen.getByTestId("popover-content")).toBeInTheDocument();
+ expect(screen.getByTestId("command")).toBeInTheDocument();
+ expect(screen.getByTestId("command-input")).toBeInTheDocument();
+ expect(screen.getByTestId("command-list")).toBeInTheDocument();
+ });
+
+ test("shows available tags excluding current tags", () => {
+ render( );
+
+ const commandItems = screen.getAllByTestId("command-item");
+ expect(commandItems).toHaveLength(2); // Should show Tag2 and Tag3 but not Tag1 (which is in currentTags)
+ expect(commandItems[0]).toHaveAttribute("data-value", "tag2");
+ expect(commandItems[1]).toHaveAttribute("data-value", "tag3");
+ });
+
+ test("calls addTag when a tag is selected", async () => {
+ const user = userEvent.setup();
+ const addTagMock = vi.fn();
+ const setOpenMock = vi.fn();
+
+ render( );
+
+ const tag2Item = screen.getAllByTestId("command-item")[0];
+ await user.click(tag2Item);
+
+ expect(addTagMock).toHaveBeenCalledWith("tag2");
+ expect(setOpenMock).toHaveBeenCalledWith(false);
+ });
+
+ test("calls createTag when Enter is pressed with a new tag", async () => {
+ const user = userEvent.setup();
+ const createTagMock = vi.fn();
+
+ render( );
+
+ const input = screen.getByTestId("command-input");
+ await user.type(input, "{enter}");
+
+ expect(createTagMock).toHaveBeenCalledWith("NewTag");
+ });
+
+ test("doesn't show create option when searchValue matches existing tag", () => {
+ render( );
+
+ const commandItems = screen.getAllByTestId("command-item");
+ expect(commandItems).toHaveLength(2); // Tag2 and Tag3
+ expect(commandItems[0]).toHaveAttribute("data-value", "tag2");
+ expect(screen.queryByRole("button", { name: /\+ Add Tag2/i })).not.toBeInTheDocument();
+ });
+
+ test("resets search value when closed", () => {
+ const setSearchValueMock = vi.fn();
+ const { rerender } = render(
+
+ );
+
+ // Change to closed state
+ rerender(
+
+ );
+
+ expect(setSearchValueMock).toHaveBeenCalledWith("");
+ });
+
+ test("updates placeholder based on available tags", () => {
+ // With available tags
+ const { rerender } = render( );
+
+ expect(screen.getByTestId("command-input")).toHaveAttribute("placeholder", "Search tags");
+
+ // Without available tags
+ rerender( );
+
+ expect(screen.getByTestId("command-input")).toHaveAttribute("placeholder", "Add tag");
+ });
+});
diff --git a/apps/web/modules/ui/components/targeting-indicator/index.test.tsx b/apps/web/modules/ui/components/targeting-indicator/index.test.tsx
new file mode 100644
index 0000000000..8e42b3f9fb
--- /dev/null
+++ b/apps/web/modules/ui/components/targeting-indicator/index.test.tsx
@@ -0,0 +1,93 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TBaseFilters, TSegment } from "@formbricks/types/segment";
+import { TargetingIndicator } from "./index";
+
+// Mock tolgee
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ "environments.surveys.edit.audience": "Audience",
+ "environments.surveys.edit.targeted": "Targeted",
+ "environments.surveys.edit.everyone": "Everyone",
+ "environments.surveys.edit.only_people_who_match_your_targeting_can_be_surveyed":
+ "Only people who match your targeting can be surveyed",
+ "environments.surveys.edit.without_a_filter_all_of_your_users_can_be_surveyed":
+ "Without a filter all of your users can be surveyed",
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
+
+describe("TargetingIndicator", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders correctly with null segment", () => {
+ render( );
+
+ expect(screen.getByText("Audience:")).toBeInTheDocument();
+ expect(screen.getByText("Everyone")).toBeInTheDocument();
+ expect(screen.getByText("Without a filter all of your users can be surveyed")).toBeInTheDocument();
+
+ // Should show the filter icon when no targeting
+ const filterIcon = document.querySelector("svg");
+ expect(filterIcon).toBeInTheDocument();
+ });
+
+ test("renders correctly with empty filters", () => {
+ const emptySegment: TSegment = {
+ id: "seg_123",
+ environmentId: "env_123",
+ title: "Test Segment",
+ description: "A test segment",
+ isPrivate: false,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ filters: [],
+ surveys: [],
+ };
+
+ render( );
+
+ expect(screen.getByText("Audience:")).toBeInTheDocument();
+ expect(screen.getByText("Everyone")).toBeInTheDocument();
+ expect(screen.getByText("Without a filter all of your users can be surveyed")).toBeInTheDocument();
+
+ // Should show the filter icon when no targeting
+ const filterIcon = document.querySelector("svg");
+ expect(filterIcon).toBeInTheDocument();
+ });
+
+ test("renders correctly with filters", () => {
+ const segmentWithFilters: TSegment = {
+ id: "seg_123",
+ environmentId: "env_123",
+ title: "Test Segment",
+ description: "A test segment",
+ isPrivate: false,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ filters: [
+ {
+ id: "filter_123",
+ },
+ ] as unknown as TBaseFilters,
+ surveys: [],
+ };
+
+ render( );
+
+ expect(screen.getByText("Audience:")).toBeInTheDocument();
+ expect(screen.getByText("Targeted")).toBeInTheDocument();
+ expect(screen.getByText("Only people who match your targeting can be surveyed")).toBeInTheDocument();
+
+ // Should show the users icon when targeting is active
+ const usersIcon = document.querySelector("svg");
+ expect(usersIcon).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/modules/ui/components/theme-styling-preview-survey/index.test.tsx b/apps/web/modules/ui/components/theme-styling-preview-survey/index.test.tsx
new file mode 100644
index 0000000000..ae6d3876d6
--- /dev/null
+++ b/apps/web/modules/ui/components/theme-styling-preview-survey/index.test.tsx
@@ -0,0 +1,302 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TSurvey } from "@formbricks/types/surveys/types";
+import { ThemeStylingPreviewSurvey } from "./index";
+
+// Mock required components
+vi.mock("@/modules/ui/components/client-logo", () => ({
+ ClientLogo: ({ projectLogo, previewSurvey }: any) => (
+
+ {projectLogo?.url ? "Logo" : "No Logo"}
+
+ ),
+}));
+
+vi.mock("@/modules/ui/components/media-background", () => ({
+ MediaBackground: ({ children, isEditorView }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("@/modules/ui/components/preview-survey/components/modal", () => ({
+ Modal: ({ children, isOpen, placement, darkOverlay, clickOutsideClose, previewMode }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("@/modules/ui/components/reset-progress-button", () => ({
+ ResetProgressButton: ({ onClick }: any) => (
+
+ Reset Progress
+
+ ),
+}));
+
+vi.mock("@/modules/ui/components/survey", () => ({
+ SurveyInline: ({ survey, isPreviewMode, isBrandingEnabled, languageCode }: any) => (
+
+ Survey Content
+
+ ),
+}));
+
+// Mock framer-motion
+vi.mock("framer-motion", async () => {
+ const actual = await vi.importActual("framer-motion");
+ return {
+ ...actual,
+ motion: {
+ div: ({ children, className, animate }: any) => (
+
+ {children}
+
+ ),
+ },
+ };
+});
+
+// Mock tolgee
+vi.mock("@tolgee/react", () => ({
+ useTranslate: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ "common.link_survey": "Link Survey",
+ "common.app_survey": "App Survey",
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
+
+describe("ThemeStylingPreviewSurvey", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockSurvey: TSurvey = {
+ id: "survey1",
+ name: "Test Survey",
+ type: "link",
+ environmentId: "env1",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ status: "draft",
+ languages: {},
+ projectOverwrites: {
+ placement: "bottomRight",
+ darkOverlay: true,
+ clickOutsideClose: true,
+ },
+ } as TSurvey;
+
+ const mockProject = {
+ id: "project1",
+ name: "Test Project",
+ placement: "center",
+ darkOverlay: false,
+ clickOutsideClose: false,
+ inAppSurveyBranding: true,
+ linkSurveyBranding: true,
+ logo: { url: "http://example.com/logo.png" },
+ styling: {
+ roundness: 8,
+ cardBackgroundColor: { light: "#ffffff" },
+ isLogoHidden: false,
+ },
+ } as any;
+
+ test("renders correctly with link survey type", () => {
+ const setPreviewType = vi.fn();
+
+ render(
+
+ );
+
+ // Check if browser header elements are rendered
+ expect(screen.getByText("Preview")).toBeInTheDocument();
+ expect(screen.getByTestId("reset-progress-button")).toBeInTheDocument();
+
+ // Check if MediaBackground is rendered for link survey
+ const mediaBackground = screen.getByTestId("media-background");
+ expect(mediaBackground).toBeInTheDocument();
+ expect(mediaBackground).toHaveAttribute("data-editor", "true");
+
+ // Check if ClientLogo is rendered
+ const clientLogo = screen.getByTestId("client-logo");
+ expect(clientLogo).toBeInTheDocument();
+ expect(clientLogo).toHaveAttribute("data-preview", "true");
+
+ // Check if SurveyInline is rendered with correct props
+ const surveyInline = screen.getByTestId("survey-inline");
+ expect(surveyInline).toBeInTheDocument();
+ expect(surveyInline).toHaveAttribute("data-survey-type", "link");
+ expect(surveyInline).toHaveAttribute("data-preview-mode", "true");
+ expect(surveyInline).toHaveAttribute("data-branding-enabled", "true");
+
+ // Check if toggle buttons are rendered
+ expect(screen.getByText("Link Survey")).toBeInTheDocument();
+ expect(screen.getByText("App Survey")).toBeInTheDocument();
+ });
+
+ test("renders correctly with app survey type", () => {
+ const setPreviewType = vi.fn();
+
+ render(
+
+ );
+
+ // Check if browser header elements are rendered
+ expect(screen.getByText("Your web app")).toBeInTheDocument();
+ expect(screen.getByTestId("reset-progress-button")).toBeInTheDocument();
+
+ // Check if Modal is rendered for app survey
+ const previewModal = screen.getByTestId("preview-modal");
+ expect(previewModal).toBeInTheDocument();
+ expect(previewModal).toHaveAttribute("data-open", "true");
+ expect(previewModal).toHaveAttribute("data-placement", "bottomRight");
+ expect(previewModal).toHaveAttribute("data-dark-overlay", "true");
+ expect(previewModal).toHaveAttribute("data-click-outside-close", "true");
+ expect(previewModal).toHaveAttribute("data-preview-mode", "desktop");
+
+ // Check if SurveyInline is rendered with correct props
+ const surveyInline = screen.getByTestId("survey-inline");
+ expect(surveyInline).toBeInTheDocument();
+ expect(surveyInline).toHaveAttribute("data-survey-type", "app");
+ expect(surveyInline).toHaveAttribute("data-preview-mode", "true");
+ expect(surveyInline).toHaveAttribute("data-branding-enabled", "true");
+ });
+
+ test("handles toggle between link and app survey types", async () => {
+ const setPreviewType = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ // Click on App Survey button
+ await user.click(screen.getByText("App Survey"));
+
+ // Check if setPreviewType was called with "app"
+ expect(setPreviewType).toHaveBeenCalledWith("app");
+
+ // Clean up and reset
+ cleanup();
+ setPreviewType.mockClear();
+
+ // Render with app type
+ render(
+
+ );
+
+ // Click on Link Survey button
+ await user.click(screen.getByText("Link Survey"));
+
+ // Check if setPreviewType was called with "link"
+ expect(setPreviewType).toHaveBeenCalledWith("link");
+ });
+
+ test("handles reset progress button click", async () => {
+ const setPreviewType = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ // Click the reset progress button
+ await user.click(screen.getByTestId("reset-progress-button"));
+
+ // Check if a new survey component renders with a new key
+ // Since we can't easily check the key directly, we can verify the content is still there
+ expect(screen.getByTestId("survey-inline")).toBeInTheDocument();
+ });
+
+ test("renders without logo when isLogoHidden is true", () => {
+ const setPreviewType = vi.fn();
+ const projectWithHiddenLogo = {
+ ...mockProject,
+ styling: {
+ ...mockProject.styling,
+ isLogoHidden: true,
+ },
+ };
+
+ render(
+
+ );
+
+ // Check that the logo is not rendered
+ expect(screen.queryByTestId("client-logo")).not.toBeInTheDocument();
+ });
+
+ test("uses project settings when projectOverwrites are not provided", () => {
+ const setPreviewType = vi.fn();
+ const surveyWithoutOverwrites = {
+ ...mockSurvey,
+ projectOverwrites: undefined,
+ };
+
+ render(
+
+ );
+
+ // Check if Modal uses project settings
+ const previewModal = screen.getByTestId("preview-modal");
+ expect(previewModal).toHaveAttribute("data-placement", "center");
+ expect(previewModal).toHaveAttribute("data-dark-overlay", "false");
+ expect(previewModal).toHaveAttribute("data-click-outside-close", "false");
+ });
+});
diff --git a/apps/web/modules/ui/components/toaster-client/index.test.tsx b/apps/web/modules/ui/components/toaster-client/index.test.tsx
new file mode 100644
index 0000000000..77edecca5c
--- /dev/null
+++ b/apps/web/modules/ui/components/toaster-client/index.test.tsx
@@ -0,0 +1,39 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import { ToasterClient } from "./index";
+
+// Mock react-hot-toast
+vi.mock("react-hot-toast", () => ({
+ Toaster: ({ toastOptions }: any) => (
+
+ Mock Toaster
+
+ ),
+}));
+
+describe("ToasterClient", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders the Toaster component", () => {
+ const { getByTestId } = render( );
+
+ const toaster = getByTestId("mock-toaster");
+ expect(toaster).toBeInTheDocument();
+ expect(toaster).toHaveTextContent("Mock Toaster");
+ });
+
+ test("passes the correct toast options to the Toaster", () => {
+ const { getByTestId } = render( );
+
+ const toaster = getByTestId("mock-toaster");
+ const toastOptions = JSON.parse(toaster.getAttribute("data-toast-options") || "{}");
+
+ expect(toastOptions).toHaveProperty("success");
+ expect(toastOptions).toHaveProperty("error");
+ expect(toastOptions.success).toHaveProperty("className", "formbricks__toast__success");
+ expect(toastOptions.error).toHaveProperty("className", "formbricks__toast__error");
+ });
+});
diff --git a/apps/web/modules/ui/components/tooltip/index.test.tsx b/apps/web/modules/ui/components/tooltip/index.test.tsx
new file mode 100644
index 0000000000..5ab04d6956
--- /dev/null
+++ b/apps/web/modules/ui/components/tooltip/index.test.tsx
@@ -0,0 +1,196 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipRenderer, TooltipTrigger } from "./index";
+
+// Mock radix-ui tooltip
+vi.mock("@radix-ui/react-tooltip", () => ({
+ Root: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+ Provider: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+ Trigger: ({ children, asChild, ...props }: any) => (
+
+ {children}
+
+ ),
+ Content: ({ children, sideOffset, className, ...props }: any) => (
+
+ {children}
+
+ ),
+ Tooltip: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+describe("Tooltip", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders basic tooltip components", () => {
+ render(
+
+
+ Hover me
+ Tooltip content
+
+
+ );
+
+ expect(screen.getByTestId("tooltip-provider")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip-root")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip-content")).toBeInTheDocument();
+
+ expect(screen.getByText("Hover me")).toBeInTheDocument();
+ expect(screen.getByText("Tooltip content")).toBeInTheDocument();
+ });
+
+ test("applies correct default classes to TooltipContent", () => {
+ render(
+
+
+ Hover me
+ Tooltip content
+
+
+ );
+
+ const contentElement = screen.getByTestId("tooltip-content");
+ expect(contentElement).toHaveClass("animate-in");
+ expect(contentElement).toHaveClass("fade-in-50");
+ expect(contentElement).toHaveClass("z-50");
+ expect(contentElement).toHaveClass("rounded-md");
+ expect(contentElement).toHaveClass("border");
+ expect(contentElement).toHaveClass("border-slate-100");
+ expect(contentElement).toHaveClass("bg-white");
+ });
+
+ test("applies custom classes to TooltipContent", () => {
+ render(
+
+
+ Hover me
+ Tooltip content
+
+
+ );
+
+ const contentElement = screen.getByTestId("tooltip-content");
+ expect(contentElement).toHaveClass("custom-class");
+ });
+
+ test("accepts custom sideOffset prop", () => {
+ render(
+
+
+ Hover me
+ Tooltip content
+
+
+ );
+
+ const contentElement = screen.getByTestId("tooltip-content");
+ expect(contentElement).toHaveAttribute("data-side-offset", "10");
+ });
+
+ test("uses default sideOffset when not provided", () => {
+ render(
+
+
+ Hover me
+ Tooltip content
+
+
+ );
+
+ const contentElement = screen.getByTestId("tooltip-content");
+ expect(contentElement).toHaveAttribute("data-side-offset", "4");
+ });
+
+ test("sets asChild prop on trigger", () => {
+ render(
+
+
+
+ Click me
+
+ Tooltip content
+
+
+ );
+
+ const triggerElement = screen.getByTestId("tooltip-trigger");
+ expect(triggerElement).toHaveAttribute("data-as-child", "true");
+ });
+});
+
+describe("TooltipRenderer", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders tooltip with content", () => {
+ render(
+
+ Trigger
+
+ );
+
+ expect(screen.getByText("Trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip-content")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Tooltip text");
+ expect(screen.getByTestId("tooltip-content")).toHaveClass("test-class");
+ });
+
+ test("applies triggerClass to the trigger wrapper", () => {
+ render(
+
+ Trigger
+
+ );
+
+ const trigger = screen.getByTestId("tooltip-trigger").firstChild;
+ expect(trigger).toHaveClass("trigger-class");
+ });
+
+ test("doesn't render tooltip when shouldRender is false", () => {
+ render(
+
+ Trigger
+
+ );
+
+ expect(screen.getByText("Trigger")).toBeInTheDocument();
+ expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("tooltip-content")).not.toBeInTheDocument();
+ });
+
+ test("renders tooltip with React node as content", () => {
+ render(
+
+ Complex tooltip content
+
+ }>
+ Trigger
+
+ );
+
+ const tooltipContent = screen.getByTestId("tooltip-content");
+ expect(tooltipContent).toBeInTheDocument();
+ expect(tooltipContent.innerHTML).toContain("Complex tooltip ");
+ expect(tooltipContent.innerHTML).toContain("content ");
+ });
+});
diff --git a/apps/web/modules/ui/components/typography/index.test.tsx b/apps/web/modules/ui/components/typography/index.test.tsx
new file mode 100644
index 0000000000..25cabf8f79
--- /dev/null
+++ b/apps/web/modules/ui/components/typography/index.test.tsx
@@ -0,0 +1,158 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { H1, H2, H3, H4, InlineCode, Large, Lead, List, Muted, P, Quote, Small } from "./index";
+
+describe("Typography Components", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders H1 correctly", () => {
+ const { container } = render(Heading 1 );
+ const h1Element = container.querySelector("h1");
+
+ expect(h1Element).toBeInTheDocument();
+ expect(h1Element).toHaveTextContent("Heading 1");
+ expect(h1Element?.className).toContain("text-4xl");
+ expect(h1Element?.className).toContain("font-bold");
+ expect(h1Element?.className).toContain("tracking-tight");
+ expect(h1Element?.className).toContain("text-slate-800");
+ });
+
+ test("renders H2 correctly", () => {
+ const { container } = render(Heading 2 );
+ const h2Element = container.querySelector("h2");
+
+ expect(h2Element).toBeInTheDocument();
+ expect(h2Element).toHaveTextContent("Heading 2");
+ expect(h2Element?.className).toContain("text-3xl");
+ expect(h2Element?.className).toContain("font-semibold");
+ expect(h2Element?.className).toContain("border-b");
+ expect(h2Element?.className).toContain("text-slate-800");
+ });
+
+ test("renders H3 correctly", () => {
+ const { container } = render(Heading 3 );
+ const h3Element = container.querySelector("h3");
+
+ expect(h3Element).toBeInTheDocument();
+ expect(h3Element).toHaveTextContent("Heading 3");
+ expect(h3Element?.className).toContain("text-2xl");
+ expect(h3Element?.className).toContain("font-semibold");
+ expect(h3Element?.className).toContain("text-slate-800");
+ });
+
+ test("renders H4 correctly", () => {
+ const { container } = render(Heading 4 );
+ const h4Element = container.querySelector("h4");
+
+ expect(h4Element).toBeInTheDocument();
+ expect(h4Element).toHaveTextContent("Heading 4");
+ expect(h4Element?.className).toContain("text-xl");
+ expect(h4Element?.className).toContain("font-semibold");
+ expect(h4Element?.className).toContain("text-slate-800");
+ });
+
+ test("renders Lead correctly", () => {
+ const { container } = render(Lead paragraph );
+ const pElement = container.querySelector("p");
+
+ expect(pElement).toBeInTheDocument();
+ expect(pElement).toHaveTextContent("Lead paragraph");
+ expect(pElement?.className).toContain("text-xl");
+ expect(pElement?.className).toContain("text-slate-800");
+ });
+
+ test("renders P correctly", () => {
+ const { container } = render(Standard paragraph
);
+ const pElement = container.querySelector("p");
+
+ expect(pElement).toBeInTheDocument();
+ expect(pElement).toHaveTextContent("Standard paragraph");
+ expect(pElement?.className).toContain("leading-7");
+ });
+
+ test("renders Large correctly", () => {
+ const { container } = render(Large text );
+ const divElement = container.querySelector("div");
+
+ expect(divElement).toBeInTheDocument();
+ expect(divElement).toHaveTextContent("Large text");
+ expect(divElement?.className).toContain("text-lg");
+ expect(divElement?.className).toContain("font-semibold");
+ });
+
+ test("renders Small correctly", () => {
+ const { container } = render(Small text );
+ const pElement = container.querySelector("p");
+
+ expect(pElement).toBeInTheDocument();
+ expect(pElement).toHaveTextContent("Small text");
+ expect(pElement?.className).toContain("text-sm");
+ expect(pElement?.className).toContain("font-medium");
+ });
+
+ test("renders Muted correctly", () => {
+ const { container } = render(Muted text );
+ const spanElement = container.querySelector("span");
+
+ expect(spanElement).toBeInTheDocument();
+ expect(spanElement).toHaveTextContent("Muted text");
+ expect(spanElement?.className).toContain("text-sm");
+ expect(spanElement?.className).toContain("text-muted-foreground");
+ });
+
+ test("renders InlineCode correctly", () => {
+ const { container } = render(code );
+ const codeElement = container.querySelector("code");
+
+ expect(codeElement).toBeInTheDocument();
+ expect(codeElement).toHaveTextContent("code");
+ expect(codeElement?.className).toContain("font-mono");
+ expect(codeElement?.className).toContain("text-sm");
+ expect(codeElement?.className).toContain("font-semibold");
+ });
+
+ test("renders List correctly", () => {
+ const { container } = render(
+
+ Item 1
+ Item 2
+
+ );
+ const ulElement = container.querySelector("ul");
+ const liElements = container.querySelectorAll("li");
+
+ expect(ulElement).toBeInTheDocument();
+ expect(liElements.length).toBe(2);
+ expect(ulElement?.className).toContain("list-disc");
+ expect(liElements[0]).toHaveTextContent("Item 1");
+ expect(liElements[1]).toHaveTextContent("Item 2");
+ });
+
+ test("renders Quote correctly", () => {
+ const { container } = render(Quoted text
);
+ const blockquoteElement = container.querySelector("blockquote");
+
+ expect(blockquoteElement).toBeInTheDocument();
+ expect(blockquoteElement).toHaveTextContent("Quoted text");
+ expect(blockquoteElement?.className).toContain("border-l-2");
+ expect(blockquoteElement?.className).toContain("italic");
+ });
+
+ test("applies custom className to components", () => {
+ const { container } = render(Custom Heading );
+ const h1Element = container.querySelector("h1");
+
+ expect(h1Element).toHaveClass("custom-class");
+ expect(h1Element).toHaveClass("text-4xl"); // Should still have default classes
+ });
+
+ test("passes additional props to components", () => {
+ const { container } = render(Test Heading );
+ const h1Element = container.querySelector("h1");
+
+ expect(h1Element).toHaveAttribute("data-testid", "test-heading");
+ });
+});
diff --git a/apps/web/modules/ui/components/upgrade-prompt/index.test.tsx b/apps/web/modules/ui/components/upgrade-prompt/index.test.tsx
new file mode 100644
index 0000000000..8d1f052585
--- /dev/null
+++ b/apps/web/modules/ui/components/upgrade-prompt/index.test.tsx
@@ -0,0 +1,132 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { UpgradePrompt } from "./index";
+
+vi.mock("@/modules/ui/components/button", () => ({
+ Button: ({ children, onClick, asChild, variant }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("lucide-react", () => ({
+ KeyIcon: () =>
,
+}));
+
+vi.mock("next/link", () => ({
+ __esModule: true,
+ default: ({ href, children, target, rel }: any) => (
+
+ {children}
+
+ ),
+}));
+
+describe("UpgradePrompt", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const mockProps = {
+ title: "Upgrade Your Account",
+ description: "Get access to premium features by upgrading your account.",
+ buttons: [
+ { text: "Upgrade Now", href: "/pricing" },
+ { text: "Learn More", href: "/features" },
+ ] as [any, any],
+ };
+
+ test("renders component with correct content", () => {
+ render( );
+
+ // Check if title and description are rendered
+ expect(screen.getByText("Upgrade Your Account")).toBeInTheDocument();
+ expect(screen.getByText("Get access to premium features by upgrading your account.")).toBeInTheDocument();
+
+ // Check if the KeyIcon is rendered
+ expect(screen.getByTestId("key-icon")).toBeInTheDocument();
+
+ // Check if buttons are rendered with correct text
+ expect(screen.getByText("Upgrade Now")).toBeInTheDocument();
+ expect(screen.getByText("Learn More")).toBeInTheDocument();
+ });
+
+ test("renders buttons with correct links", () => {
+ render( );
+
+ // Check if buttons have correct href attributes
+ const primaryLink = screen.getByText("Upgrade Now").closest("a");
+ const secondaryLink = screen.getByText("Learn More").closest("a");
+
+ expect(primaryLink).toHaveAttribute("href", "/pricing");
+ expect(secondaryLink).toHaveAttribute("href", "/features");
+
+ // Check if links have correct attributes
+ expect(primaryLink).toHaveAttribute("target", "_blank");
+ expect(primaryLink).toHaveAttribute("rel", "noopener noreferrer");
+ expect(secondaryLink).toHaveAttribute("target", "_blank");
+ expect(secondaryLink).toHaveAttribute("rel", "noopener noreferrer");
+ });
+
+ test("handles onClick for buttons without href", async () => {
+ const primaryOnClick = vi.fn();
+ const secondaryOnClick = vi.fn();
+ const user = userEvent.setup();
+
+ const propsWithClickHandlers = {
+ ...mockProps,
+ buttons: [
+ { text: "Primary Action", onClick: primaryOnClick },
+ { text: "Secondary Action", onClick: secondaryOnClick },
+ ] as [any, any],
+ };
+
+ render( );
+
+ // Click the buttons and check if handlers are called
+ await user.click(screen.getByText("Primary Action"));
+ await user.click(screen.getByText("Secondary Action"));
+
+ expect(primaryOnClick).toHaveBeenCalledTimes(1);
+ expect(secondaryOnClick).toHaveBeenCalledTimes(1);
+ });
+
+ test("renders with mixed button types (href and onClick)", () => {
+ const secondaryOnClick = vi.fn();
+
+ const mixedProps = {
+ ...mockProps,
+ buttons: [
+ { text: "Primary Link", href: "/primary" },
+ { text: "Secondary Button", onClick: secondaryOnClick },
+ ] as [any, any],
+ };
+
+ render( );
+
+ // Check primary button is a link
+ const primaryButton = screen.getByText("Primary Link");
+ expect(primaryButton.closest("a")).toHaveAttribute("href", "/primary");
+
+ // Check secondary button is not a link
+ const secondaryButton = screen.getByText("Secondary Button");
+ expect(secondaryButton.closest("a")).toBeNull();
+ });
+
+ test("applies asChild and variant correctly to buttons", () => {
+ render( );
+
+ // In our mock, we're checking data attributes that represent the props
+ const primaryButton = screen.getByText("Upgrade Now").closest("button");
+ const secondaryButton = screen.getByText("Learn More").closest("button");
+
+ expect(primaryButton).toHaveAttribute("data-as-child", "true");
+ expect(primaryButton).toHaveAttribute("data-variant", "default");
+
+ expect(secondaryButton).toHaveAttribute("data-as-child", "true");
+ expect(secondaryButton).toHaveAttribute("data-variant", "secondary");
+ });
+});
diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts
index 61b119cb9b..c55aebc052 100644
--- a/apps/web/vite.config.mts
+++ b/apps/web/vite.config.mts
@@ -61,6 +61,7 @@ export default defineConfig({
"modules/setup/**/signup/**",
"modules/setup/**/layout.tsx",
"modules/survey/follow-ups/**",
+ "modules/ui/components/icons/**",
"app/share/**",
"lib/shortUrl/**",
"modules/ee/contacts/[contactId]/**",
diff --git a/packages/types/surveys/types.ts b/packages/types/surveys/types.ts
index e15b12302c..0cf0dc529a 100644
--- a/packages/types/surveys/types.ts
+++ b/packages/types/surveys/types.ts
@@ -274,6 +274,8 @@ export const ZSurveyPictureChoice = z.object({
imageUrl: z.string(),
});
+export type TSurveyPictureChoice = z.infer;
+
export type TSurveyQuestionChoice = z.infer;
// Logic types
diff --git a/sonar-project.properties b/sonar-project.properties
index e4bde865b0..96b0b03ebe 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false
sonar.sourceEncoding=UTF-8
# Coverage
-sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**
-sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**
\ No newline at end of file
+sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,apps/web/modules/ui/components/icons/**
+sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,apps/web/modules/ui/components/icons/**
\ No newline at end of file