)}
-
+ >
);
};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.tsx
index 3d72b38aef..7d824216a9 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.tsx
@@ -11,7 +11,7 @@ export const AppTab = () => {
const [selectedTab, setSelectedTab] = useState("webapp");
return (
-
+
{
expect(link).toHaveTextContent("environments.surveys.share.dynamic_popup.alert_button");
});
- test("renders title with correct text", () => {
- render();
-
- const h3 = screen.getByTestId("h3");
- expect(h3).toBeInTheDocument();
- expect(h3).toHaveTextContent("environments.surveys.share.dynamic_popup.title");
- });
-
test("renders attribute-based targeting documentation button", () => {
render();
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.tsx
index 95c8910fc6..0d95846afc 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.tsx
@@ -1,7 +1,6 @@
"use client";
import { DocumentationLinks } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links";
-import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { useTranslate } from "@tolgee/react";
import Link from "next/link";
@@ -15,39 +14,33 @@ export const DynamicPopupTab = ({ environmentId, surveyId }: DynamicPopupTabProp
const { t } = useTranslate();
return (
-
-
-
- {t("environments.surveys.share.dynamic_popup.alert_title")}
-
- {t("environments.surveys.share.dynamic_popup.alert_description")}
-
-
-
- {t("environments.surveys.share.dynamic_popup.alert_button")}
-
-
-
+
+
+ {t("environments.surveys.share.dynamic_popup.alert_title")}
+ {t("environments.surveys.share.dynamic_popup.alert_description")}
+
+
+ {t("environments.surveys.share.dynamic_popup.alert_button")}
+
+
+
-
-
-
+
+
);
};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.tsx
index bf744c0c8b..4ccd6cbbe6 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.tsx
@@ -1,6 +1,5 @@
"use client";
-import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { CodeBlock } from "@/modules/ui/components/code-block";
@@ -142,19 +141,15 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => {
};
return (
-
-
-
-
{renderTabContent()}
-
-
+
+
+
{renderTabContent()}
+
);
};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx
index 790bf73ab7..e7e0ad8d83 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx
@@ -192,13 +192,6 @@ describe("PersonalLinksTab", () => {
cleanup();
});
- test("renders the component with correct title and description", () => {
- render();
-
- expect(screen.getByText("environments.surveys.share.personal_links.title")).toBeInTheDocument();
- expect(screen.getByText("environments.surveys.share.personal_links.description")).toBeInTheDocument();
- });
-
test("renders recipients section with segment selection", () => {
render();
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.tsx
index c15c7140e8..85d2b53aae 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.tsx
@@ -27,7 +27,6 @@ import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { TSegment } from "@formbricks/types/segment";
import { generatePersonalLinksAction } from "../../actions";
-import { TabContainer } from "./tab-container";
interface PersonalLinksTabProps {
environmentId: string;
@@ -169,86 +168,82 @@ export const PersonalLinksTab = ({
return (
-
-
- {/* Recipients Section */}
- (
-
- {t("common.recipients")}
-
-
-
-
- {t("environments.surveys.share.personal_links.create_and_manage_segments")}
-
-
- )}
- />
-
- {/* Expiry Date Section */}
- (
-
- {t("environments.surveys.share.personal_links.expiry_date_optional")}
-
-
-
-
- {t("environments.surveys.share.personal_links.expiry_date_description")}
-
-
- )}
- />
-
- {/* Generate Button */}
-
-
-
-
- {/* Info Box */}
-
+ {/* Recipients Section */}
+ (
+
+ {t("common.recipients")}
+
+
+
+
+ {t("environments.surveys.share.personal_links.create_and_manage_segments")}
+
+
+ )}
/>
-
+
+ {/* Expiry Date Section */}
+ (
+
+ {t("environments.surveys.share.personal_links.expiry_date_optional")}
+
+
+
+
+ {t("environments.surveys.share.personal_links.expiry_date_description")}
+
+
+ )}
+ />
+
+ {/* Generate Button */}
+
+
+
+
+ {/* Info Box */}
+
);
};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx
index 05c0264951..c8492f5f42 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx
@@ -157,30 +157,6 @@ describe("QRCodeTab", () => {
cleanup();
});
- describe("Component rendering", () => {
- test("renders component with title and description", () => {
- render(
);
-
- expect(
- screen.getByText("environments.surveys.summary.make_survey_accessible_via_qr_code")
- ).toBeInTheDocument();
- expect(
- screen.getByText("environments.surveys.summary.responses_collected_via_qr_code_are_anonymous")
- ).toBeInTheDocument();
- });
-
- test("renders without QR code when surveyUrl is empty", () => {
- render(
);
-
- expect(
- screen.getByText("environments.surveys.summary.make_survey_accessible_via_qr_code")
- ).toBeInTheDocument();
- expect(
- screen.getByText("environments.surveys.summary.responses_collected_via_qr_code_are_anonymous")
- ).toBeInTheDocument();
- });
- });
-
describe("QR Code generation", () => {
test("attempts to generate QR code when surveyUrl is provided", async () => {
render(
);
@@ -256,12 +232,6 @@ describe("QRCodeTab", () => {
test("shows appropriate state when surveyUrl is empty", async () => {
render(
);
- // Component should render some content
- await waitFor(() => {
- const content = screen.getByText("environments.surveys.summary.make_survey_accessible_via_qr_code");
- expect(content).toBeInTheDocument();
- });
-
// Should show button (but disabled) when URL is empty, no alert
const button = screen.getByTestId("button");
expect(button).toBeInTheDocument();
@@ -287,20 +257,6 @@ describe("QRCodeTab", () => {
expect(screen.getByTestId("button")).toBeInTheDocument();
});
});
-
- test("handles empty surveyUrl gracefully", async () => {
- render(
);
-
- // Component should render basic content even with empty URL
- await waitFor(() => {
- const title = screen.getByText("environments.surveys.summary.make_survey_accessible_via_qr_code");
- const description = screen.getByText(
- "environments.surveys.summary.responses_collected_via_qr_code_are_anonymous"
- );
- expect(title).toBeInTheDocument();
- expect(description).toBeInTheDocument();
- });
- });
});
describe("Accessibility", () => {
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.tsx
index fe639384ab..8589f81b60 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.tsx
@@ -1,6 +1,5 @@
"use client";
-import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
import { getQRCodeOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/get-qr-code-options";
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
@@ -75,46 +74,42 @@ export const QRCodeTab = ({ surveyUrl }: QRCodeTabProps) => {
};
return (
-
-
- {isLoading && (
-
-
-
{t("environments.surveys.summary.generating_qr_code")}
-
- )}
+ <>
+ {isLoading && (
+
+
+
{t("environments.surveys.summary.generating_qr_code")}
+
+ )}
- {hasError && (
-
- {t("common.something_went_wrong")}
- {t("environments.surveys.summary.qr_code_generation_failed")}
-
- )}
+ {hasError && (
+
+ {t("common.something_went_wrong")}
+ {t("environments.surveys.summary.qr_code_generation_failed")}
+
+ )}
- {!isLoading && !hasError && (
-
-
-
+ {!isLoading && !hasError && (
+
+
+
+ )}
+ >
);
};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx
index a2370ea583..3b86a33f1a 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx
@@ -1,11 +1,56 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { afterEach, describe, expect, test, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { ShareViewType } from "../../types/share";
import { ShareView } from "./share-view";
+// Mock sidebar components
+vi.mock("@/modules/ui/components/sidebar", () => ({
+ SidebarProvider: ({ children, open, className, style }: any) => (
+
+ {children}
+
+ ),
+ Sidebar: ({ children }: { children: React.ReactNode }) => {children}
,
+ SidebarContent: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ SidebarGroup: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ SidebarGroupContent: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ SidebarGroupLabel: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ SidebarMenu: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ SidebarMenuItem: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ SidebarMenuButton: ({
+ children,
+ onClick,
+ tooltip,
+ className,
+ isActive,
+ }: {
+ children: React.ReactNode;
+ onClick: () => void;
+ tooltip: string;
+ className?: string;
+ isActive?: boolean;
+ }) => (
+
+ ),
+}));
+
// Mock child components
vi.mock("./app-tab", () => ({
AppTab: () => AppTab Content
,
@@ -79,13 +124,6 @@ vi.mock("@/modules/ui/components/upgrade-prompt", () => ({
),
}));
-// Mock @tolgee/react
-vi.mock("@tolgee/react", () => ({
- useTranslate: () => ({
- t: (key: string) => key,
- }),
-}));
-
// Mock lucide-react
vi.mock("lucide-react", () => ({
CopyIcon: () => CopyIcon
,
@@ -118,33 +156,6 @@ vi.mock("lucide-react", () => ({
),
}));
-// Mock sidebar components
-vi.mock("@/modules/ui/components/sidebar", () => ({
- SidebarProvider: ({ children }: { children: React.ReactNode }) => {children}
,
- Sidebar: ({ children }: { children: React.ReactNode }) => {children}
,
- SidebarContent: ({ children }: { children: React.ReactNode }) => {children}
,
- SidebarGroup: ({ children }: { children: React.ReactNode }) => {children}
,
- SidebarGroupContent: ({ children }: { children: React.ReactNode }) => {children}
,
- SidebarGroupLabel: ({ children }: { children: React.ReactNode }) => {children}
,
- SidebarMenu: ({ children }: { children: React.ReactNode }) => {children}
,
- SidebarMenuItem: ({ children }: { children: React.ReactNode }) => {children}
,
- SidebarMenuButton: ({
- children,
- onClick,
- tooltip,
- className,
- }: {
- children: React.ReactNode;
- onClick: () => void;
- tooltip: string;
- className?: string;
- }) => (
-
- ),
-}));
-
// Mock tooltip and typography components
vi.mock("@/modules/ui/components/tooltip", () => ({
TooltipRenderer: ({ children }: { children: React.ReactNode }) => {children}
,
@@ -178,21 +189,69 @@ vi.mock("@/lib/cn", () => ({
cn: (...args: any[]) => args.filter(Boolean).join(" "),
}));
-const mockTabs: Array<{ id: ShareViewType; label: string; icon: React.ElementType }> = [
- { id: ShareViewType.EMAIL, label: "Email", icon: () => },
+const mockTabs: Array<{
+ id: ShareViewType;
+ label: string;
+ icon: React.ElementType;
+ componentType: React.ComponentType;
+ componentProps: any;
+ title: string;
+ description?: string;
+}> = [
+ {
+ id: ShareViewType.EMAIL,
+ label: "Email",
+ icon: () => ,
+ componentType: () => Email Content
,
+ componentProps: {},
+ title: "Email",
+ description: "Email Description",
+ },
{
id: ShareViewType.WEBSITE_EMBED,
label: "Website Embed",
icon: () => ,
+ componentType: () => Website Embed Content
,
+ componentProps: {},
+ title: "Website Embed",
+ description: "Website Embed Description",
},
{
id: ShareViewType.DYNAMIC_POPUP,
label: "Dynamic Popup",
icon: () => ,
+ componentType: () => Dynamic Popup Content
,
+ componentProps: {},
+ title: "Dynamic Popup",
+ description: "Dynamic Popup Description",
+ },
+ {
+ id: ShareViewType.ANON_LINKS,
+ label: "Anonymous Links",
+ icon: () => ,
+ componentType: () => Anonymous Links Content
,
+ componentProps: {},
+ title: "Anonymous Links",
+ description: "Anonymous Links Description",
+ },
+ {
+ id: ShareViewType.QR_CODE,
+ label: "QR Code",
+ icon: () => ,
+ componentType: () => QR Code Content
,
+ componentProps: {},
+ title: "QR Code",
+ description: "QR Code Description",
+ },
+ {
+ id: ShareViewType.APP,
+ label: "App",
+ icon: () => ,
+ componentType: () => App Content
,
+ componentProps: {},
+ title: "App",
+ description: "App Description",
},
- { id: ShareViewType.ANON_LINKS, label: "Anonymous Links", icon: () => },
- { id: ShareViewType.QR_CODE, label: "QR Code", icon: () => },
- { id: ShareViewType.APP, label: "App", icon: () => },
];
const mockSurveyLink = {
@@ -254,7 +313,20 @@ const defaultProps = {
isFormbricksCloud: false,
};
+// Mock window object for resize testing
+Object.defineProperty(window, "innerWidth", {
+ writable: true,
+ configurable: true,
+ value: 1024,
+});
+
describe("ShareView", () => {
+ beforeEach(() => {
+ // Reset window size to default before each test
+ window.innerWidth = 1024;
+ vi.clearAllMocks();
+ });
+
afterEach(() => {
cleanup();
vi.clearAllMocks();
@@ -285,58 +357,6 @@ describe("ShareView", () => {
expect(defaultProps.setActiveId).toHaveBeenCalledWith(ShareViewType.WEBSITE_EMBED);
});
- test("renders EmailTab when activeId is 'email'", () => {
- render();
- expect(screen.getByTestId("email-tab")).toBeInTheDocument();
- expect(
- screen.getByText(`EmailTab Content for ${defaultProps.survey.id} with ${defaultProps.email}`)
- ).toBeInTheDocument();
- });
-
- test("renders WebsiteEmbedTab when activeId is 'website-embed'", () => {
- render();
- expect(screen.getByTestId("website-embed-tab")).toBeInTheDocument();
- expect(screen.getByText(`WebsiteEmbedTab Content for ${defaultProps.surveyUrl}`)).toBeInTheDocument();
- });
-
- test("renders DynamicPopupTab when activeId is 'dynamic-popup'", () => {
- render();
- expect(screen.getByTestId("dynamic-popup-tab")).toBeInTheDocument();
- expect(
- screen.getByText(
- `DynamicPopupTab Content for ${defaultProps.survey.id} in ${defaultProps.environmentId}`
- )
- ).toBeInTheDocument();
- });
-
- test("renders AnonymousLinksTab when activeId is 'anon-links'", () => {
- render();
- expect(screen.getByTestId("anonymous-links-tab")).toBeInTheDocument();
- expect(
- screen.getByText(`AnonymousLinksTab Content for ${defaultProps.survey.id} at ${defaultProps.surveyUrl}`)
- ).toBeInTheDocument();
- });
-
- test("renders QRCodeTab when activeId is 'qr-code'", () => {
- render();
- expect(screen.getByTestId("qr-code-tab")).toBeInTheDocument();
- });
-
- test("renders AppTab when activeId is 'app'", () => {
- render();
- expect(screen.getByTestId("app-tab")).toBeInTheDocument();
- });
-
- test("renders PersonalLinksTab when activeId is 'personal-links'", () => {
- render();
- expect(screen.getByTestId("personal-links-tab")).toBeInTheDocument();
- expect(
- screen.getByText(
- `PersonalLinksTab Content for ${defaultProps.survey.id} in ${defaultProps.environmentId}`
- )
- ).toBeInTheDocument();
- });
-
test("calls setActiveId when a responsive tab is clicked", async () => {
render();
@@ -400,4 +420,269 @@ describe("ShareView", () => {
);
}
});
+
+ describe("Responsive Behavior", () => {
+ test("detects large screen size on mount", () => {
+ window.innerWidth = 1200;
+ render();
+
+ // SidebarProvider should be rendered with open=true for large screens
+ const sidebarProvider = screen.getByTestId("sidebar-provider");
+ expect(sidebarProvider).toHaveAttribute("data-open", "true");
+ });
+
+ test("detects small screen size on mount", () => {
+ window.innerWidth = 800;
+ render();
+
+ // SidebarProvider should be rendered with open=false for small screens
+ const sidebarProvider = screen.getByTestId("sidebar-provider");
+ expect(sidebarProvider).toHaveAttribute("data-open", "false");
+ });
+
+ test("updates screen size on window resize", async () => {
+ window.innerWidth = 1200;
+ const { rerender } = render();
+
+ // Initially large screen
+ let sidebarProvider = screen.getByTestId("sidebar-provider");
+ expect(sidebarProvider).toHaveAttribute("data-open", "true");
+
+ // Simulate window resize to small screen
+ window.innerWidth = 800;
+ window.dispatchEvent(new Event("resize"));
+
+ // Force re-render to trigger useEffect
+ rerender();
+
+ // Should now be small screen
+ sidebarProvider = screen.getByTestId("sidebar-provider");
+ expect(sidebarProvider).toHaveAttribute("data-open", "false");
+ });
+
+ test("cleans up resize listener on unmount", () => {
+ const removeEventListenerSpy = vi.spyOn(window, "removeEventListener");
+ const { unmount } = render();
+
+ unmount();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith("resize", expect.any(Function));
+ });
+ });
+
+ describe("TabContainer Integration", () => {
+ test("renders active tab with correct title and description", () => {
+ render();
+
+ const tabContainer = screen.getByTestId("tab-container");
+ expect(tabContainer).toBeInTheDocument();
+
+ const tabTitle = screen.getByTestId("tab-title");
+ expect(tabTitle).toHaveTextContent("Email");
+
+ const tabDescription = screen.getByTestId("tab-description");
+ expect(tabDescription).toHaveTextContent("Email Description");
+
+ const tabContent = screen.getByTestId("email-tab-content");
+ expect(tabContent).toBeInTheDocument();
+ });
+
+ test("renders different tab when activeId changes", () => {
+ const { rerender } = render();
+
+ // Initially shows Email tab
+ expect(screen.getByTestId("tab-title")).toHaveTextContent("Email");
+ expect(screen.getByTestId("email-tab-content")).toBeInTheDocument();
+
+ // Change to Website Embed tab
+ rerender();
+
+ expect(screen.getByTestId("tab-title")).toHaveTextContent("Website Embed");
+ expect(screen.getByTestId("website-embed-tab-content")).toBeInTheDocument();
+ expect(screen.queryByTestId("email-tab-content")).not.toBeInTheDocument();
+ });
+
+ test("handles tab without description", () => {
+ const tabsWithoutDescription = [
+ {
+ id: ShareViewType.EMAIL,
+ label: "Email",
+ icon: () => ,
+ componentType: () => Email Content
,
+ componentProps: {},
+ title: "Email",
+ // No description property
+ },
+ ];
+
+ render();
+
+ const tabDescription = screen.getByTestId("tab-description");
+ expect(tabDescription).toHaveTextContent("");
+ });
+
+ test("returns null when no active tab is found", () => {
+ const emptyTabs: typeof mockTabs = [];
+
+ render();
+
+ const tabContainer = screen.queryByTestId("tab-container");
+ expect(tabContainer).not.toBeInTheDocument();
+ });
+ });
+
+ describe("SidebarProvider Configuration", () => {
+ test("renders SidebarProvider with correct props for link surveys", () => {
+ render();
+
+ const sidebarProvider = screen.getByTestId("sidebar-provider");
+ expect(sidebarProvider).toBeInTheDocument();
+ expect(sidebarProvider).toHaveAttribute("data-open", "true");
+ expect(sidebarProvider).toHaveClass("flex min-h-0 w-auto lg:col-span-1");
+ expect(sidebarProvider).toHaveStyle("--sidebar-width: 100%");
+ });
+
+ test("does not render SidebarProvider for non-link surveys", () => {
+ render();
+
+ expect(screen.queryByTestId("sidebar-provider")).not.toBeInTheDocument();
+ });
+
+ test("renders correct grid layout for link surveys", () => {
+ render();
+
+ const container = screen.getByTestId("sidebar-provider").parentElement;
+ expect(container).toHaveClass("lg:grid lg:grid-cols-4");
+ });
+
+ test("does not render grid layout for non-link surveys", () => {
+ const { container } = render();
+
+ const mainDiv = container.querySelector(".h-full > div");
+ expect(mainDiv).not.toHaveClass("lg:grid lg:grid-cols-4");
+ });
+ });
+
+ describe("Sidebar Menu Buttons", () => {
+ test("renders SidebarMenuButton with correct isActive prop", () => {
+ render();
+
+ const emailButton = screen.getByLabelText("Email");
+ expect(emailButton).toHaveAttribute("data-active", "true");
+
+ const websiteEmbedButton = screen.getByLabelText("Website Embed");
+ expect(websiteEmbedButton).toHaveAttribute("data-active", "false");
+ });
+
+ test("renders all tabs in sidebar menu", () => {
+ render();
+
+ mockTabs.forEach((tab) => {
+ const button = screen.getByLabelText(tab.label);
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveAttribute("data-active", tab.id === ShareViewType.EMAIL ? "true" : "false");
+ });
+ });
+ });
+
+ describe("Mobile Responsive Buttons", () => {
+ test("renders mobile buttons for all tabs", () => {
+ render();
+
+ // Mobile buttons should be present for all tabs
+ mockTabs.forEach((tab) => {
+ // Map ShareViewType to actual testid used in the component
+ const testIdMap: Record = {
+ [ShareViewType.ANON_LINKS]: "link-tab-icon",
+ [ShareViewType.PERSONAL_LINKS]: "personal-links-tab-icon",
+ [ShareViewType.WEBSITE_EMBED]: "website-embed-tab-icon",
+ [ShareViewType.EMAIL]: "email-tab-icon",
+ [ShareViewType.SOCIAL_MEDIA]: "social-media-tab-icon",
+ [ShareViewType.QR_CODE]: "qr-code-tab-icon",
+ [ShareViewType.DYNAMIC_POPUP]: "dynamic-popup-tab-icon",
+ [ShareViewType.APP]: "app-tab-icon",
+ };
+
+ const expectedTestId = testIdMap[tab.id] || `${tab.id}-tab-icon`;
+ const mobileButtons = screen.getAllByTestId(expectedTestId);
+ const mobileButton = mobileButtons.find((icon) => {
+ const button = icon.closest("button");
+ return button && button.getAttribute("data-variant") === "ghost";
+ });
+ expect(mobileButton).toBeInTheDocument();
+ });
+ });
+
+ test("applies correct classes to mobile buttons based on active state", () => {
+ render();
+
+ const websiteEmbedIcons = screen.getAllByTestId("website-embed-tab-icon");
+ const activeMobileButton = websiteEmbedIcons
+ .find((icon) => {
+ const button = icon.closest("button");
+ return button && button.getAttribute("data-variant") === "ghost";
+ })
+ ?.closest("button");
+
+ if (activeMobileButton) {
+ expect(activeMobileButton).toHaveClass("bg-white text-slate-900 shadow-sm hover:bg-white");
+ }
+ });
+ });
+
+ describe("Content Area Layout", () => {
+ test("applies correct column span for link surveys", () => {
+ const { container } = render();
+
+ const contentArea = container.querySelector('[class*="lg:col-span-3"]');
+ expect(contentArea).toBeInTheDocument();
+ expect(contentArea).toHaveClass("lg:col-span-3");
+ });
+
+ test("does not apply column span for non-link surveys", () => {
+ const { container } = render();
+
+ const contentArea = container.querySelector('[class*="lg:col-span-3"]');
+ expect(contentArea).toBeNull();
+ });
+
+ test("renders mobile button container with correct visibility class", () => {
+ const { container } = render();
+
+ const mobileButtonContainer = container.querySelector(".md\\:hidden");
+ expect(mobileButtonContainer).toBeInTheDocument();
+ expect(mobileButtonContainer).toHaveClass("md:hidden");
+ });
+ });
+
+ describe("Enhanced Tab Structure", () => {
+ test("handles tabs with all required properties", () => {
+ const completeTab = {
+ id: ShareViewType.EMAIL,
+ label: "Test Email",
+ icon: () => ,
+ componentType: () => Test Content
,
+ componentProps: {},
+ title: "Test Title",
+ description: "Test Description",
+ };
+
+ render();
+
+ expect(screen.getByTestId("tab-title")).toHaveTextContent("Test Title");
+ expect(screen.getByTestId("tab-description")).toHaveTextContent("Test Description");
+ expect(screen.getByTestId("test-content")).toBeInTheDocument();
+ });
+
+ test("uses title from tab definition in TabContainer", () => {
+ const customTitleTab = {
+ ...mockTabs[0],
+ title: "Custom Email Title",
+ };
+
+ render();
+
+ expect(screen.getByTestId("tab-title")).toHaveTextContent("Custom Email Title");
+ });
+ });
});
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.tsx
index 58b8bb320f..c6abec1f6a 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.tsx
@@ -1,7 +1,6 @@
"use client";
-import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab";
-import { QRCodeTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab";
+import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
import { ShareViewType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share";
import { cn } from "@/lib/cn";
import { Button } from "@/modules/ui/components/button";
@@ -20,46 +19,24 @@ import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { Small } from "@/modules/ui/components/typography";
import { useTranslate } from "@tolgee/react";
import { useEffect, useState } from "react";
-import { TSegment } from "@formbricks/types/segment";
import { TSurvey } from "@formbricks/types/surveys/types";
-import { TUserLocale } from "@formbricks/types/user";
-import { AnonymousLinksTab } from "./anonymous-links-tab";
-import { AppTab } from "./app-tab";
-import { EmailTab } from "./email-tab";
-import { PersonalLinksTab } from "./personal-links-tab";
-import { WebsiteEmbedTab } from "./website-embed-tab";
interface ShareViewProps {
- tabs: Array<{ id: ShareViewType; label: string; icon: React.ElementType }>;
+ tabs: Array<{
+ id: ShareViewType;
+ label: string;
+ icon: React.ElementType;
+ componentType: React.ComponentType;
+ componentProps: any;
+ title: string;
+ description?: string;
+ }>;
activeId: ShareViewType;
setActiveId: React.Dispatch>;
- environmentId: string;
survey: TSurvey;
- email: string;
- surveyUrl: string;
- publicDomain: string;
- setSurveyUrl: React.Dispatch>;
- locale: TUserLocale;
- segments: TSegment[];
- isContactsEnabled: boolean;
- isFormbricksCloud: boolean;
}
-export const ShareView = ({
- tabs,
- activeId,
- setActiveId,
- environmentId,
- survey,
- email,
- surveyUrl,
- publicDomain,
- setSurveyUrl,
- locale,
- segments,
- isContactsEnabled,
- isFormbricksCloud,
-}: ShareViewProps) => {
+export const ShareView = ({ tabs, activeId, setActiveId, survey }: ShareViewProps) => {
const { t } = useTranslate();
const [isLargeScreen, setIsLargeScreen] = useState(true);
@@ -76,40 +53,16 @@ export const ShareView = ({
}, []);
const renderActiveTab = () => {
- switch (activeId) {
- case ShareViewType.EMAIL:
- return ;
- case ShareViewType.WEBSITE_EMBED:
- return ;
- case ShareViewType.DYNAMIC_POPUP:
- return ;
- case ShareViewType.ANON_LINKS:
- return (
-
- );
- case ShareViewType.APP:
- return ;
- case ShareViewType.QR_CODE:
- return ;
- case ShareViewType.PERSONAL_LINKS:
- return (
-
- );
- default:
- return null;
- }
+ const activeTab = tabs.find((tab) => tab.id === activeId);
+ if (!activeTab) return null;
+
+ const { componentType: Component, componentProps } = activeTab;
+
+ return (
+
+
+
+ );
};
return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx
new file mode 100644
index 0000000000..aad6e879d2
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx
@@ -0,0 +1,138 @@
+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 { SocialMediaTab } from "./social-media-tab";
+
+// Mock next/link
+vi.mock("next/link", () => ({
+ default: ({ href, children, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+// Mock window.open
+Object.defineProperty(window, "open", {
+ writable: true,
+ value: vi.fn(),
+});
+
+const mockSurveyUrl = "https://app.formbricks.com/s/survey1";
+const mockSurveyTitle = "Test Survey";
+
+const expectedPlatforms = [
+ { name: "LinkedIn", description: "Share on LinkedIn" },
+ { name: "Threads", description: "Share on Threads" },
+ { name: "Facebook", description: "Share on Facebook" },
+ { name: "Reddit", description: "Share on Reddit" },
+ { name: "X", description: "Share on X (formerly Twitter)" },
+];
+
+describe("SocialMediaTab", () => {
+ afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+ });
+
+ test("renders all social media platforms with correct names", () => {
+ render();
+
+ expectedPlatforms.forEach((platform) => {
+ expect(screen.getByText(platform.name)).toBeInTheDocument();
+ });
+ });
+
+ test("renders source tracking alert with correct content", () => {
+ render();
+
+ expect(
+ screen.getByText("environments.surveys.share.social_media.source_tracking_enabled")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("environments.surveys.share.social_media.source_tracking_enabled_alert_description")
+ ).toBeInTheDocument();
+ expect(screen.getByText("common.learn_more")).toBeInTheDocument();
+
+ const learnMoreButton = screen.getByRole("button", { name: "common.learn_more" });
+ expect(learnMoreButton).toBeInTheDocument();
+ });
+
+ test("renders platform buttons for all platforms", () => {
+ render();
+
+ const platformButtons = expectedPlatforms.map((platform) =>
+ screen.getByRole("button", { name: new RegExp(platform.name, "i") })
+ );
+ expect(platformButtons).toHaveLength(expectedPlatforms.length);
+ });
+
+ test("opens sharing window when LinkedIn button is clicked", async () => {
+ const mockWindowOpen = vi.spyOn(window, "open");
+ render();
+
+ const linkedInButton = screen.getByRole("button", { name: /linkedin/i });
+ await userEvent.click(linkedInButton);
+
+ expect(mockWindowOpen).toHaveBeenCalledWith(
+ expect.stringContaining("linkedin.com/shareArticle"),
+ "share-dialog",
+ "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes"
+ );
+ });
+
+ test("includes source tracking in shared URLs", async () => {
+ const mockWindowOpen = vi.spyOn(window, "open");
+ render();
+
+ const linkedInButton = screen.getByRole("button", { name: /linkedin/i });
+ await userEvent.click(linkedInButton);
+
+ const calledUrl = mockWindowOpen.mock.calls[0][0] as string;
+ const decodedUrl = decodeURIComponent(calledUrl);
+ expect(decodedUrl).toContain("source=linkedin");
+ });
+
+ test("opens sharing window when Facebook button is clicked", async () => {
+ const mockWindowOpen = vi.spyOn(window, "open");
+ render();
+
+ const facebookButton = screen.getByRole("button", { name: /facebook/i });
+ await userEvent.click(facebookButton);
+
+ expect(mockWindowOpen).toHaveBeenCalledWith(
+ expect.stringContaining("facebook.com/sharer"),
+ "share-dialog",
+ "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes"
+ );
+ });
+
+ test("opens sharing window when X button is clicked", async () => {
+ const mockWindowOpen = vi.spyOn(window, "open");
+ render();
+
+ const xButton = screen.getByRole("button", { name: /^x$/i });
+ await userEvent.click(xButton);
+
+ expect(mockWindowOpen).toHaveBeenCalledWith(
+ expect.stringContaining("twitter.com/intent/tweet"),
+ "share-dialog",
+ "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes"
+ );
+ });
+
+ test("encodes URLs and titles correctly for sharing", async () => {
+ const specialCharUrl = "https://app.formbricks.com/s/survey1?param=test&other=value";
+ const specialCharTitle = "Test Survey & More";
+ const mockWindowOpen = vi.spyOn(window, "open");
+
+ render();
+
+ const linkedInButton = screen.getByRole("button", { name: /linkedin/i });
+ await userEvent.click(linkedInButton);
+
+ const calledUrl = mockWindowOpen.mock.calls[0][0] as string;
+ expect(calledUrl).toContain(encodeURIComponent(specialCharTitle));
+ });
+});
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.tsx
new file mode 100644
index 0000000000..d64f3ca367
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
+import { Button } from "@/modules/ui/components/button";
+import { FacebookIcon } from "@/modules/ui/components/icons/facebook-icon";
+import { LinkedinIcon } from "@/modules/ui/components/icons/linkedin-icon";
+import { RedditIcon } from "@/modules/ui/components/icons/reddit-icon";
+import { ThreadsIcon } from "@/modules/ui/components/icons/threads-icon";
+import { XIcon } from "@/modules/ui/components/icons/x-icon";
+import { useTranslate } from "@tolgee/react";
+import { AlertCircleIcon } from "lucide-react";
+import { useMemo } from "react";
+
+interface SocialMediaTabProps {
+ surveyUrl: string;
+ surveyTitle: string;
+}
+
+export const SocialMediaTab: React.FC = ({ surveyUrl, surveyTitle }) => {
+ const { t } = useTranslate();
+
+ const socialMediaPlatforms = useMemo(() => {
+ const shareText = surveyTitle;
+
+ // Add source tracking to the survey URL
+ const getTrackedUrl = (platform: string) => {
+ const sourceParam = `source=${platform.toLowerCase()}`;
+ const separator = surveyUrl.includes("?") ? "&" : "?";
+ return `${surveyUrl}${separator}${sourceParam}`;
+ };
+
+ return [
+ {
+ id: "linkedin",
+ name: "LinkedIn",
+ icon: ,
+ url: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(getTrackedUrl("linkedin"))}&title=${encodeURIComponent(shareText)}`,
+ description: "Share on LinkedIn",
+ },
+ {
+ id: "threads",
+ name: "Threads",
+ icon: ,
+ url: `https://www.threads.net/intent/post?text=${encodeURIComponent(shareText)}%20${encodeURIComponent(getTrackedUrl("threads"))}`,
+ description: "Share on Threads",
+ },
+ {
+ id: "facebook",
+ name: "Facebook",
+ icon: ,
+ url: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(getTrackedUrl("facebook"))}`,
+ description: "Share on Facebook",
+ },
+ {
+ id: "reddit",
+ name: "Reddit",
+ icon: ,
+ url: `https://www.reddit.com/submit?url=${encodeURIComponent(getTrackedUrl("reddit"))}&title=${encodeURIComponent(shareText)}`,
+ description: "Share on Reddit",
+ },
+ {
+ id: "x",
+ name: "X",
+ icon: ,
+ url: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(getTrackedUrl("x"))}`,
+ description: "Share on X (formerly Twitter)",
+ },
+ ];
+ }, [surveyUrl, surveyTitle]);
+
+ const handleSocialShare = (url: string) => {
+ // Open sharing window
+ window.open(
+ url,
+ "share-dialog",
+ "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes"
+ );
+ };
+
+ return (
+ <>
+
+ {socialMediaPlatforms.map((platform) => (
+
+ ))}
+
+
+
+
+ {t("environments.surveys.share.social_media.source_tracking_enabled")}
+
+ {t("environments.surveys.share.social_media.source_tracking_enabled_alert_description")}
+
+ {
+ window.open(
+ "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/source-tracking",
+ "_blank",
+ "noopener,noreferrer"
+ );
+ }}>
+ {t("common.learn_more")}
+
+
+ >
+ );
+};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.tsx
index 33b42f76c7..54b9757879 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.tsx
@@ -7,7 +7,6 @@ import { useTranslate } from "@tolgee/react";
import { CopyIcon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
-import { TabContainer } from "./tab-container";
interface WebsiteEmbedTabProps {
surveyUrl: string;
@@ -25,9 +24,7 @@ export const WebsiteEmbedTab = ({ surveyUrl }: WebsiteEmbedTabProps) => {
`;
return (
-
+ <>
{iframeCode}
@@ -50,6 +47,6 @@ export const WebsiteEmbedTab = ({ surveyUrl }: WebsiteEmbedTabProps) => {
{t("common.copy_code")}
-
+ >
);
};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share.ts
index 7691f13742..94bccb0b7d 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share.ts
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share.ts
@@ -6,5 +6,6 @@ export enum ShareViewType {
APP = "app",
WEBSITE_EMBED = "website-embed",
DYNAMIC_POPUP = "dynamic-popup",
+ SOCIAL_MEDIA = "social-media",
QR_CODE = "qr-code",
}
diff --git a/apps/web/lib/responses.test.ts b/apps/web/lib/responses.test.ts
index d534f8c46c..9c32e5ed2f 100644
--- a/apps/web/lib/responses.test.ts
+++ b/apps/web/lib/responses.test.ts
@@ -9,6 +9,11 @@ vi.mock("@/lib/utils/recall", () => ({
vi.mock("./i18n/utils", () => ({
getLocalizedValue: vi.fn((obj, lang) => obj[lang] || obj.default),
+ getLanguageCode: vi.fn((surveyLanguages, languageCode) => {
+ if (!surveyLanguages?.length || !languageCode) return null; // Changed from "default" to null
+ const language = surveyLanguages.find((surveyLanguage) => surveyLanguage.language.code === languageCode);
+ return language?.default ? "default" : language?.language.code || "default";
+ }),
}));
describe("Response Processing", () => {
@@ -43,6 +48,16 @@ describe("Response Processing", () => {
test("should return empty string for unsupported types", () => {
expect(processResponseData(undefined as any)).toBe("");
});
+
+ test("should filter out null values from array", () => {
+ const input = ["a", null, "c"] as any;
+ expect(processResponseData(input)).toBe("a; c");
+ });
+
+ test("should filter out undefined values from array", () => {
+ const input = ["a", undefined, "c"] as any;
+ expect(processResponseData(input)).toBe("a; c");
+ });
});
describe("convertResponseValue", () => {
@@ -125,6 +140,22 @@ describe("Response Processing", () => {
expect(convertResponseValue("invalid", mockPictureSelectionQuestion)).toEqual([]);
});
+ test("should handle pictureSelection type with number input", () => {
+ expect(convertResponseValue(42, mockPictureSelectionQuestion)).toEqual([]);
+ });
+
+ test("should handle pictureSelection type with object input", () => {
+ expect(convertResponseValue({ key: "value" }, mockPictureSelectionQuestion)).toEqual([]);
+ });
+
+ test("should handle pictureSelection type with null input", () => {
+ expect(convertResponseValue(null as any, mockPictureSelectionQuestion)).toEqual([]);
+ });
+
+ test("should handle pictureSelection type with undefined input", () => {
+ expect(convertResponseValue(undefined as any, mockPictureSelectionQuestion)).toEqual([]);
+ });
+
test("should handle default case with string input", () => {
expect(convertResponseValue("answer", mockOpenTextQuestion)).toBe("answer");
});
@@ -320,6 +351,32 @@ describe("Response Processing", () => {
charLimit: { enabled: false },
},
],
+ languages: [
+ {
+ language: {
+ id: "lang1",
+ code: "default",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ alias: null,
+ projectId: "proj1",
+ },
+ default: true,
+ enabled: true,
+ },
+ {
+ language: {
+ id: "lang2",
+ code: "en",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ alias: null,
+ projectId: "proj1",
+ },
+ default: false,
+ enabled: true,
+ },
+ ],
};
const response = {
id: "response1",
@@ -349,5 +406,102 @@ describe("Response Processing", () => {
const mapping = getQuestionResponseMapping(survey, response);
expect(mapping[0].question).toBe("Question 1 EN");
});
+
+ test("should handle null response language", () => {
+ const response = {
+ id: "response1",
+ surveyId: "survey1",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ finished: true,
+ data: { q1: "Answer 1" },
+ language: null,
+ meta: {
+ url: undefined,
+ country: undefined,
+ action: undefined,
+ source: undefined,
+ userAgent: undefined,
+ },
+ notes: [],
+ tags: [],
+ person: null,
+ personAttributes: {},
+ ttc: {},
+ variables: {},
+ contact: null,
+ contactAttributes: {},
+ singleUseId: null,
+ };
+ const mapping = getQuestionResponseMapping(mockSurvey, response);
+ expect(mapping).toHaveLength(2);
+ expect(mapping[0].question).toBe("Question 1");
+ });
+
+ test("should handle undefined response language", () => {
+ const response = {
+ id: "response1",
+ surveyId: "survey1",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ finished: true,
+ data: { q1: "Answer 1" },
+ language: null,
+ meta: {
+ url: undefined,
+ country: undefined,
+ action: undefined,
+ source: undefined,
+ userAgent: undefined,
+ },
+ notes: [],
+ tags: [],
+ person: null,
+ personAttributes: {},
+ ttc: {},
+ variables: {},
+ contact: null,
+ contactAttributes: {},
+ singleUseId: null,
+ };
+ const mapping = getQuestionResponseMapping(mockSurvey, response);
+ expect(mapping).toHaveLength(2);
+ expect(mapping[0].question).toBe("Question 1");
+ });
+
+ test("should handle empty survey languages", () => {
+ const survey = {
+ ...mockSurvey,
+ languages: [], // Empty languages array
+ };
+ const response = {
+ id: "response1",
+ surveyId: "survey1",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ finished: true,
+ data: { q1: "Answer 1" },
+ language: "en",
+ meta: {
+ url: undefined,
+ country: undefined,
+ action: undefined,
+ source: undefined,
+ userAgent: undefined,
+ },
+ notes: [],
+ tags: [],
+ person: null,
+ personAttributes: {},
+ ttc: {},
+ variables: {},
+ contact: null,
+ contactAttributes: {},
+ singleUseId: null,
+ };
+ const mapping = getQuestionResponseMapping(survey, response);
+ expect(mapping).toHaveLength(2);
+ expect(mapping[0].question).toBe("Question 1"); // Should fallback to default
+ });
});
});
diff --git a/apps/web/lib/responses.ts b/apps/web/lib/responses.ts
index e5e4f7e9f7..e8760e1377 100644
--- a/apps/web/lib/responses.ts
+++ b/apps/web/lib/responses.ts
@@ -1,7 +1,7 @@
import { parseRecallInfo } from "@/lib/utils/recall";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys/types";
-import { getLocalizedValue } from "./i18n/utils";
+import { getLanguageCode, getLocalizedValue } from "./i18n/utils";
// function to convert response value of type string | number | string[] or Record
to string | string[]
export const convertResponseValue = (
@@ -39,12 +39,14 @@ export const getQuestionResponseMapping = (
response: string | string[];
type: TSurveyQuestionType;
}[] = [];
+ const responseLanguageCode = getLanguageCode(survey.languages, response.language);
+
for (const question of survey.questions) {
const answer = response.data[question.id];
questionResponseMapping.push({
question: parseRecallInfo(
- getLocalizedValue(question.headline, response.language ?? "default"),
+ getLocalizedValue(question.headline, responseLanguageCode ?? "default"),
response.data
),
response: convertResponseValue(answer, question),
diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json
index 7a5b48e922..65318f1080 100644
--- a/apps/web/locales/de-DE.json
+++ b/apps/web/locales/de-DE.json
@@ -1799,7 +1799,14 @@
"send_preview_email": "Vorschau-E-Mail senden",
"title": "Binden Sie Ihre Umfrage in eine E-Mail ein"
},
- "share_view_title": "Teilen über"
+ "share_view_title": "Teilen über",
+ "social_media": {
+ "description": "Erhalte Rückmeldungen von deinen Kontakten auf verschiedenen sozialen Medien.",
+ "share_your_survey_on_social_media": "Teilen Sie Ihre Umfrage in sozialen Medien",
+ "source_tracking_enabled": "Quellenverfolgung aktiviert",
+ "source_tracking_enabled_alert_description": "Wenn Sie aus diesem Dialogfenster teilen, wird das soziale Netzwerk an den Umfragelink angehängt, sodass Sie wissen, welche Antworten über welches Netzwerk eingegangen sind.",
+ "title": "Soziale Medien"
+ }
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "Filter hinzugefügt für Antworten, bei denen die Antwort auf Frage {questionIdx} {filterComboBoxValue} - {filterValue} ist",
@@ -1849,6 +1856,7 @@
"publish_to_web_warning": "Du bist dabei, diese Umfrageergebnisse öffentlich zugänglich zu machen.",
"publish_to_web_warning_description": "Deine Umfrageergebnisse werden öffentlich sein. Jeder außerhalb deiner Organisation kann darauf zugreifen, wenn er den Link hat.",
"qr_code": "QR-Code",
+ "qr_code_description": "Antworten, die per QR-Code gesammelt werden, sind anonym.",
"qr_code_download_failed": "QR-Code-Download fehlgeschlagen",
"qr_code_download_with_start_soon": "QR Code-Download startet bald",
"qr_code_generation_failed": "Es gab ein Problem beim Laden des QR-Codes für die Umfrage. Bitte versuchen Sie es erneut.",
diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json
index 0e30c60adb..b9c2d7313d 100644
--- a/apps/web/locales/en-US.json
+++ b/apps/web/locales/en-US.json
@@ -1799,7 +1799,14 @@
"send_preview_email": "Send preview email",
"title": "Embed your survey in an email"
},
- "share_view_title": "Share via"
+ "share_view_title": "Share via",
+ "social_media": {
+ "description": "Get responses from your contacts on various social media networks.",
+ "share_your_survey_on_social_media": "Share your survey on social media",
+ "source_tracking_enabled": "Source tracking enabled",
+ "source_tracking_enabled_alert_description": "When sharing from this dialog, the social media network will be appended to the survey link so you know which responses came via each network.",
+ "title": "Social media"
+ }
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "Added filter for responses where answer to question {questionIdx} is {filterComboBoxValue} - {filterValue} ",
@@ -1849,6 +1856,7 @@
"publish_to_web_warning": "You are about to release these survey results to the public.",
"publish_to_web_warning_description": "Your survey results will be public. Anyone outside your organization can access them if they have the link.",
"qr_code": "QR code",
+ "qr_code_description": "Responses collected via QR code are anonymous.",
"qr_code_download_failed": "QR code download failed",
"qr_code_download_with_start_soon": "QR code download will start soon",
"qr_code_generation_failed": "There was a problem, loading the survey QR Code. Please try again.",
diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json
index 9e117d0d97..afc56be799 100644
--- a/apps/web/locales/fr-FR.json
+++ b/apps/web/locales/fr-FR.json
@@ -1799,7 +1799,14 @@
"send_preview_email": "Envoyer un e-mail d'aperçu",
"title": "Intégrez votre sondage dans un e-mail"
},
- "share_view_title": "Partager par"
+ "share_view_title": "Partager par",
+ "social_media": {
+ "description": "Obtenez des réponses de vos contacts sur divers réseaux sociaux.",
+ "share_your_survey_on_social_media": "Partagez votre sondage sur les réseaux sociaux",
+ "source_tracking_enabled": "Suivi des sources activé",
+ "source_tracking_enabled_alert_description": "En partageant depuis cette boîte de dialogue, le réseau social sera ajouté au lien du sondage afin que vous sachiez quelles réponses proviennent de chaque réseau.",
+ "title": "Médias sociaux"
+ }
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "Filtre ajouté pour les réponses où la réponse à la question '{'questionIdx'}' est '{'filterComboBoxValue'}' - '{'filterValue'}' ",
@@ -1849,6 +1856,7 @@
"publish_to_web_warning": "Vous êtes sur le point de rendre ces résultats d'enquête publics.",
"publish_to_web_warning_description": "Les résultats de votre enquête seront publics. Toute personne en dehors de votre organisation pourra y accéder si elle a le lien.",
"qr_code": "Code QR",
+ "qr_code_description": "Les réponses collectées via le code QR sont anonymes.",
"qr_code_download_failed": "Échec du téléchargement du code QR",
"qr_code_download_with_start_soon": "Le téléchargement du code QR débutera bientôt",
"qr_code_generation_failed": "\"Un problème est survenu lors du chargement du code QR du sondage. Veuillez réessayer.\"",
diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json
index ea434840c4..563990f8a9 100644
--- a/apps/web/locales/pt-BR.json
+++ b/apps/web/locales/pt-BR.json
@@ -1799,7 +1799,14 @@
"send_preview_email": "Enviar prévia de e-mail",
"title": "Incorpore sua pesquisa em um e-mail"
},
- "share_view_title": "Compartilhar via"
+ "share_view_title": "Compartilhar via",
+ "social_media": {
+ "description": "Obtenha respostas de seus contatos em várias redes sociais.",
+ "share_your_survey_on_social_media": "Compartilhe sua pesquisa nas redes sociais",
+ "source_tracking_enabled": "rastreamento de origem ativado",
+ "source_tracking_enabled_alert_description": "Ao compartilhar a partir deste diálogo, a rede social será adicionada ao link da pesquisa para que você saiba de qual rede vieram as respostas.",
+ "title": "Mídia Social"
+ }
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ",
@@ -1849,6 +1856,7 @@
"publish_to_web_warning": "Você está prestes a divulgar esses resultados da pesquisa para o público.",
"publish_to_web_warning_description": "Os resultados da sua pesquisa serão públicos. Qualquer pessoa fora da sua organização pode acessá-los se tiver o link.",
"qr_code": "Código QR",
+ "qr_code_description": "Respostas coletadas via código QR são anônimas.",
"qr_code_download_failed": "falha no download do código QR",
"qr_code_download_with_start_soon": "O download do código QR começará em breve",
"qr_code_generation_failed": "Houve um problema ao carregar o Código QR do questionário. Por favor, tente novamente.",
diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json
index 9d3324fd36..c3bcc5ab6b 100644
--- a/apps/web/locales/pt-PT.json
+++ b/apps/web/locales/pt-PT.json
@@ -1799,7 +1799,14 @@
"send_preview_email": "Enviar pré-visualização de email",
"title": "Incorporar o seu inquérito num email"
},
- "share_view_title": "Partilhar via"
+ "share_view_title": "Partilhar via",
+ "social_media": {
+ "description": "Obtenha respostas dos seus contactos em várias redes sociais.",
+ "share_your_survey_on_social_media": "Partilhe o seu inquérito nas redes sociais",
+ "source_tracking_enabled": "Rastreamento de origem ativado",
+ "source_tracking_enabled_alert_description": "Ao partilhar a partir deste diálogo, a rede social será anexada ao link do inquérito para que saiba de que rede vieram as respostas.",
+ "title": "Redes Sociais"
+ }
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ",
@@ -1849,6 +1856,7 @@
"publish_to_web_warning": "Está prestes a divulgar estes resultados do inquérito ao público.",
"publish_to_web_warning_description": "Os resultados do seu inquérito serão públicos. Qualquer pessoa fora da sua organização pode aceder a eles se tiver o link.",
"qr_code": "Código QR",
+ "qr_code_description": "Respostas recolhidas através de código QR são anónimas.",
"qr_code_download_failed": "Falha ao transferir o código QR",
"qr_code_download_with_start_soon": "O download do código QR começará em breve",
"qr_code_generation_failed": "Ocorreu um problema ao carregar o Código QR do questionário. Por favor, tente novamente.",
diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json
index 22f80e174f..6885fb0dad 100644
--- a/apps/web/locales/zh-Hant-TW.json
+++ b/apps/web/locales/zh-Hant-TW.json
@@ -1799,7 +1799,14 @@
"send_preview_email": "發送預覽電子郵件",
"title": "嵌入 你的 調查 在 電子郵件 中"
},
- "share_view_title": "透過 分享"
+ "share_view_title": "透過 分享",
+ "social_media": {
+ "description": "從 您 的 聯絡人 在 各 種 社交 媒體 網絡 上 獲得 回應。",
+ "share_your_survey_on_social_media": "分享 您 的 問卷 在 社交媒體 上",
+ "source_tracking_enabled": "來源追蹤已啟用",
+ "source_tracking_enabled_alert_description": "從 此 對 話 框 共 享 時,社 交 媒 體 網 絡 會 被 附 加 到 調 查鏈 接 下,讓 您 知 道 各 網 絡 的 回 應 來 源。",
+ "title": "社群媒體"
+ }
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "已新增回應的篩選器,其中問題 '{'questionIdx'}' 的答案為 '{'filterComboBoxValue'}' - '{'filterValue'}'",
@@ -1849,6 +1856,7 @@
"publish_to_web_warning": "您即將將這些問卷結果發布到公共領域。",
"publish_to_web_warning_description": "您的問卷結果將會是公開的。任何組織外的人員都可以存取這些結果(如果他們有連結)。",
"qr_code": "QR 碼",
+ "qr_code_description": "透過 QR code 收集的回應都是匿名的。",
"qr_code_download_failed": "QR code 下載失敗",
"qr_code_download_with_start_soon": "QR code 下載即將開始",
"qr_code_generation_failed": "載入調查 QR Code 時發生問題。請再試一次。",
diff --git a/apps/web/modules/email/components/email-button.tsx b/apps/web/modules/email/components/email-button.tsx
index c767edd9c7..814e801bbe 100644
--- a/apps/web/modules/email/components/email-button.tsx
+++ b/apps/web/modules/email/components/email-button.tsx
@@ -8,7 +8,7 @@ interface EmailButtonProps {
export function EmailButton({ label, href }: EmailButtonProps): React.JSX.Element {
return (
-