diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.tsx index dfb1f2107e..0885289369 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/components/SettingsCard.tsx @@ -2,6 +2,7 @@ import { cn } from "@/lib/cn"; import { Badge } from "@/modules/ui/components/badge"; +import { H3, Small } from "@/modules/ui/components/typography"; import { useTranslate } from "@tolgee/react"; export const SettingsCard = ({ @@ -31,7 +32,7 @@ export const SettingsCard = ({ id={title}>
-

{title}

+

{title}

{beta && } {soon && ( @@ -39,7 +40,9 @@ export const SettingsCard = ({ )}
-

{description}

+ + {description} +
{children}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx index 33cfd44518..66cd1e6ae7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx @@ -217,8 +217,10 @@ describe("ShareEmbedSurvey", () => { tabs: { id: string; label: string; icon: LucideIcon }[]; activeId: string; }; - expect(embedViewProps.tabs.length).toBe(4); + expect(embedViewProps.tabs.length).toBe(5); expect(embedViewProps.tabs.find((tab) => tab.id === "app")).toBeUndefined(); + expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeDefined(); + expect(embedViewProps.tabs.find((tab) => tab.id === "website-embed")).toBeDefined(); expect(embedViewProps.tabs[0].id).toBe("link"); expect(embedViewProps.activeId).toBe("link"); }); @@ -230,7 +232,9 @@ describe("ShareEmbedSurvey", () => { activeId: string; }; expect(embedViewProps.tabs.length).toBe(1); - expect(embedViewProps.tabs[0].id).toBe("app"); + expect(embedViewProps.tabs.find((tab) => tab.id === "app")).toBeDefined(); + expect(embedViewProps.tabs.find((tab) => tab.id === "website-embed")).toBeUndefined(); + expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeUndefined(); expect(embedViewProps.activeId).toBe("app"); }); @@ -285,4 +289,32 @@ describe("ShareEmbedSurvey", () => { linkTab = embedViewProps.tabs.find((tab) => tab.id === "link"); expect(linkTab?.label).toBe("environments.surveys.summary.single_use_links"); }); + + test("dynamic popup tab is only visible for link surveys", () => { + // Test link survey includes dynamic popup tab + render(); + let embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { + tabs: { id: string; label: string }[]; + }; + expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeDefined(); + cleanup(); + vi.mocked(mockShareViewComponent).mockClear(); + + // Test web survey excludes dynamic popup tab + render(); + embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { + tabs: { id: string; label: string }[]; + }; + expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeUndefined(); + }); + + test("website-embed and dynamic-popup tabs replace old webpage tab", () => { + render(); + const embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { + tabs: { id: string; label: string }[]; + }; + expect(embedViewProps.tabs.find((tab) => tab.id === "webpage")).toBeUndefined(); + expect(embedViewProps.tabs.find((tab) => tab.id === "website-embed")).toBeDefined(); + expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeDefined(); + }); }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.tsx index f960272e9a..3188f284cd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.tsx @@ -1,9 +1,10 @@ "use client"; import { getSurveyUrl } from "@/modules/analysis/utils"; -import { Dialog, DialogContent } from "@/modules/ui/components/dialog"; +import { Dialog, DialogContent, DialogTitle } from "@/modules/ui/components/dialog"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { useTranslate } from "@tolgee/react"; -import { Code2Icon, LinkIcon, MailIcon, SmartphoneIcon, UserIcon } from "lucide-react"; +import { Code2Icon, LinkIcon, MailIcon, SmartphoneIcon, SquareStack, UserIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { logger } from "@formbricks/logger"; import { TSegment } from "@formbricks/types/segment"; @@ -18,7 +19,8 @@ enum ShareViewType { LINK = "link", PERSONAL_LINKS = "personal-links", EMAIL = "email", - WEBPAGE = "webpage", + WEBSITE_EMBED = "website-embed", + DYNAMIC_POPUP = "dynamic-popup", APP = "app", } @@ -67,10 +69,15 @@ export const ShareSurveyModal = ({ icon: MailIcon, }, { - id: ShareViewType.WEBPAGE, + id: ShareViewType.WEBSITE_EMBED, label: t("environments.surveys.summary.embed_on_website"), icon: Code2Icon, }, + { + id: ShareViewType.DYNAMIC_POPUP, + label: t("environments.surveys.summary.dynamic_popup"), + icon: SquareStack, + }, ], [t, isSingleUseLinkSurvey] ); @@ -126,7 +133,10 @@ export const ShareSurveyModal = ({ return ( - + + + + {showView === "start" ? ( ({ + Alert: (props: { variant?: string; size?: string; children: React.ReactNode }) => ( +
+ {props.children} +
+ ), + AlertButton: (props: { asChild?: boolean; children: React.ReactNode }) => ( +
+ {props.children} +
+ ), + AlertDescription: (props: { children: React.ReactNode }) => ( +
{props.children}
+ ), + AlertTitle: (props: { children: React.ReactNode }) =>
{props.children}
, +})); + +vi.mock("@/modules/ui/components/button", () => ({ + Button: (props: { variant?: string; asChild?: boolean; children: React.ReactNode }) => ( +
+ {props.children} +
+ ), +})); + +vi.mock("@/modules/ui/components/typography", () => ({ + H4: (props: { children: React.ReactNode }) =>
{props.children}
, +})); + +vi.mock("@tolgee/react", () => ({ + useTranslate: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock("lucide-react", () => ({ + ExternalLinkIcon: (props: { className?: string }) => ( +
+ ExternalLinkIcon +
+ ), +})); + +// Mock Next.js Link +vi.mock("next/link", () => ({ + default: (props: { href: string; target?: string; className?: string; children: React.ReactNode }) => ( + + {props.children} + + ), +})); + +describe("DynamicPopupTab", () => { + afterEach(() => { + cleanup(); + }); + + const defaultProps = { + environmentId: "env-123", + surveyId: "survey-123", + }; + + test("renders alert with correct props", () => { + render(); + + const alert = screen.getByTestId("alert"); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveAttribute("data-variant", "info"); + expect(alert).toHaveAttribute("data-size", "default"); + }); + + test("renders alert title with translation key", () => { + render(); + + const alertTitle = screen.getByTestId("alert-title"); + expect(alertTitle).toBeInTheDocument(); + expect(alertTitle).toHaveTextContent("environments.surveys.summary.dynamic_popup.alert_title"); + }); + + test("renders alert description with translation key", () => { + render(); + + const alertDescription = screen.getByTestId("alert-description"); + expect(alertDescription).toBeInTheDocument(); + expect(alertDescription).toHaveTextContent( + "environments.surveys.summary.dynamic_popup.alert_description" + ); + }); + + test("renders alert button with link to survey edit page", () => { + render(); + + const alertButton = screen.getByTestId("alert-button"); + expect(alertButton).toBeInTheDocument(); + expect(alertButton).toHaveAttribute("data-as-child", "true"); + + const link = screen.getAllByTestId("next-link")[0]; + expect(link).toHaveAttribute("href", "/environments/env-123/surveys/survey-123/edit"); + expect(link).toHaveTextContent("environments.surveys.summary.dynamic_popup.alert_button"); + }); + + test("renders title with correct text", () => { + render(); + + const h4 = screen.getByTestId("h4"); + expect(h4).toBeInTheDocument(); + expect(h4).toHaveTextContent("environments.surveys.summary.dynamic_popup.title"); + }); + + test("renders attribute-based targeting documentation button", () => { + render(); + + const links = screen.getAllByTestId("next-link"); + const attributeLink = links.find((link) => link.getAttribute("href")?.includes("advanced-targeting")); + + expect(attributeLink).toBeInTheDocument(); + expect(attributeLink).toHaveAttribute( + "href", + "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting" + ); + expect(attributeLink).toHaveAttribute("target", "_blank"); + }); + + test("renders code and no code triggers documentation button", () => { + render(); + + const links = screen.getAllByTestId("next-link"); + const actionsLink = links.find((link) => link.getAttribute("href")?.includes("actions")); + + expect(actionsLink).toBeInTheDocument(); + expect(actionsLink).toHaveAttribute( + "href", + "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions" + ); + expect(actionsLink).toHaveAttribute("target", "_blank"); + }); + + test("renders recontact options documentation button", () => { + render(); + + const links = screen.getAllByTestId("next-link"); + const recontactLink = links.find((link) => link.getAttribute("href")?.includes("recontact")); + + expect(recontactLink).toBeInTheDocument(); + expect(recontactLink).toHaveAttribute( + "href", + "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/recontact" + ); + expect(recontactLink).toHaveAttribute("target", "_blank"); + }); + + test("all documentation buttons have external link icons", () => { + render(); + + const externalLinkIcons = screen.getAllByTestId("external-link-icon"); + expect(externalLinkIcons).toHaveLength(3); + + externalLinkIcons.forEach((icon) => { + expect(icon).toHaveClass("h-4 w-4 flex-shrink-0"); + }); + }); + + test("documentation button links open in new tab", () => { + render(); + + const documentationLinks = screen.getAllByTestId("next-link").slice(1, 4); // Skip the alert button link + + documentationLinks.forEach((link) => { + expect(link).toHaveAttribute("target", "_blank"); + }); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/DynamicPopupTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/DynamicPopupTab.tsx new file mode 100644 index 0000000000..ff57677daa --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/DynamicPopupTab.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert"; +import { Button } from "@/modules/ui/components/button"; +import { H4 } from "@/modules/ui/components/typography"; +import { useTranslate } from "@tolgee/react"; +import { ExternalLinkIcon } from "lucide-react"; +import Link from "next/link"; + +interface DynamicPopupTabProps { + environmentId: string; + surveyId: string; +} + +interface DocumentationButtonProps { + href: string; + title: string; + readDocsText: string; +} + +const DocumentationButton = ({ href, title, readDocsText }: DocumentationButtonProps) => { + return ( + + ); +}; + +export const DynamicPopupTab = ({ environmentId, surveyId }: DynamicPopupTabProps) => { + const { t } = useTranslate(); + + return ( +
+ + {t("environments.surveys.summary.dynamic_popup.alert_title")} + + {t("environments.surveys.summary.dynamic_popup.alert_description")} + + + + {t("environments.surveys.summary.dynamic_popup.alert_button")} + + + + +
+

{t("environments.surveys.summary.dynamic_popup.title")}

+ + + +
+
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/TabContainer.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/TabContainer.test.tsx new file mode 100644 index 0000000000..e4faeefe8b --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/TabContainer.test.tsx @@ -0,0 +1,75 @@ +import "@testing-library/jest-dom/vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TabContainer } from "./TabContainer"; + +// Mock components +vi.mock("@/modules/ui/components/typography", () => ({ + H3: (props: { children: React.ReactNode }) =>

{props.children}

, + Small: (props: { color?: string; margin?: string; children: React.ReactNode }) => ( +

+ {props.children} +

+ ), +})); + +describe("TabContainer", () => { + afterEach(() => { + cleanup(); + }); + + const defaultProps = { + title: "Test Tab Title", + description: "Test tab description", + children:
Tab content
, + }; + + test("renders title with correct props", () => { + render(); + + const title = screen.getByTestId("h3"); + expect(title).toBeInTheDocument(); + expect(title).toHaveTextContent("Test Tab Title"); + }); + + test("renders description with correct text and props", () => { + render(); + + const description = screen.getByTestId("small"); + expect(description).toBeInTheDocument(); + expect(description).toHaveTextContent("Test tab description"); + expect(description).toHaveAttribute("data-color", "muted"); + expect(description).toHaveAttribute("data-margin", "headerDescription"); + }); + + test("renders children content", () => { + render(); + + const tabContent = screen.getByTestId("tab-content"); + expect(tabContent).toBeInTheDocument(); + expect(tabContent).toHaveTextContent("Tab content"); + }); + + test("renders with correct container structure", () => { + render(); + + const container = screen.getByTestId("h3").parentElement?.parentElement; + expect(container).toHaveClass("flex", "h-full", "grow", "flex-col", "items-start", "space-y-4"); + }); + + test("renders header with correct structure", () => { + render(); + + const header = screen.getByTestId("h3").parentElement; + expect(header).toBeInTheDocument(); + expect(header).toContainElement(screen.getByTestId("h3")); + expect(header).toContainElement(screen.getByTestId("small")); + }); + + test("renders children directly in container", () => { + render(); + + const container = screen.getByTestId("h3").parentElement?.parentElement; + expect(container).toContainElement(screen.getByTestId("tab-content")); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/TabContainer.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/TabContainer.tsx new file mode 100644 index 0000000000..35720a3cfe --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/TabContainer.tsx @@ -0,0 +1,21 @@ +import { H3, Small } from "@/modules/ui/components/typography"; + +interface TabContainerProps { + title: string; + description: string; + children: React.ReactNode; +} + +export const TabContainer = ({ title, description, children }: TabContainerProps) => { + return ( +
+
+

{title}

+ + {description} + +
+ {children} +
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteEmbedTab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteEmbedTab.test.tsx new file mode 100644 index 0000000000..2e580c4e64 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteEmbedTab.test.tsx @@ -0,0 +1,192 @@ +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 { WebsiteEmbedTab } from "./WebsiteEmbedTab"; + +// Mock components +vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({ + AdvancedOptionToggle: (props: { + htmlId: string; + isChecked: boolean; + onToggle: (checked: boolean) => void; + title: string; + description: string; + customContainerClass?: string; + }) => ( +
+ + props.onToggle(e.target.checked)} + data-testid="embed-mode-toggle" + /> + {props.description} + {props.customContainerClass && ( + {props.customContainerClass} + )} +
+ ), +})); + +vi.mock("@/modules/ui/components/button", () => ({ + Button: (props: { + title?: string; + "aria-label"?: string; + onClick?: () => void; + children: React.ReactNode; + type?: "button" | "submit" | "reset"; + }) => ( + + ), +})); + +vi.mock("@/modules/ui/components/code-block", () => ({ + CodeBlock: (props: { + language: string; + showCopyToClipboard: boolean; + noMargin?: boolean; + children: string; + }) => ( +
+ {props.language} + {props.showCopyToClipboard.toString()} + {props.noMargin && true} +
{props.children}
+
+ ), +})); + +vi.mock("@tolgee/react", () => ({ + useTranslate: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock("lucide-react", () => ({ + CopyIcon: () =>
CopyIcon
, +})); + +// Mock react-hot-toast +vi.mock("react-hot-toast", () => ({ + default: { + success: vi.fn(), + }, +})); + +// Mock clipboard API +Object.assign(navigator, { + clipboard: { + writeText: vi.fn().mockImplementation(() => Promise.resolve()), + }, +}); + +describe("WebsiteEmbedTab", () => { + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + const defaultProps = { + surveyUrl: "https://example.com/survey/123", + }; + + test("renders all components correctly", () => { + render(); + + expect(screen.getByTestId("code-block")).toBeInTheDocument(); + expect(screen.getByTestId("advanced-option-toggle")).toBeInTheDocument(); + expect(screen.getByTestId("copy-button")).toBeInTheDocument(); + expect(screen.getByTestId("copy-icon")).toBeInTheDocument(); + }); + + test("renders correct iframe code without embed mode", () => { + render(); + + const codeBlock = screen.getByTestId("code-block"); + expect(codeBlock).toBeInTheDocument(); + + const code = codeBlock.querySelector("pre")?.textContent; + expect(code).toContain(defaultProps.surveyUrl); + expect(code).toContain(" { + render(); + + const toggle = screen.getByTestId("embed-mode-toggle"); + await userEvent.click(toggle); + + const codeBlock = screen.getByTestId("code-block"); + const code = codeBlock.querySelector("pre")?.textContent; + expect(code).toContain('src="https://example.com/survey/123?embed=true"'); + }); + + test("toggle changes embed mode state", async () => { + render(); + + const toggle = screen.getByTestId("embed-mode-toggle"); + expect(toggle).not.toBeChecked(); + + await userEvent.click(toggle); + expect(toggle).toBeChecked(); + + await userEvent.click(toggle); + expect(toggle).not.toBeChecked(); + }); + + test("copy button copies iframe code to clipboard", async () => { + render(); + + const copyButton = screen.getByTestId("copy-button"); + await userEvent.click(copyButton); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + expect.stringContaining(defaultProps.surveyUrl) + ); + const toast = await import("react-hot-toast"); + expect(toast.default.success).toHaveBeenCalledWith( + "environments.surveys.summary.embed_code_copied_to_clipboard" + ); + }); + + test("copy button copies correct code with embed mode enabled", async () => { + render(); + + const toggle = screen.getByTestId("embed-mode-toggle"); + await userEvent.click(toggle); + + const copyButton = screen.getByTestId("copy-button"); + await userEvent.click(copyButton); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expect.stringContaining("?embed=true")); + }); + + test("renders code block with correct props", () => { + render(); + + expect(screen.getByTestId("language")).toHaveTextContent("html"); + expect(screen.getByTestId("show-copy")).toHaveTextContent("false"); + expect(screen.getByTestId("no-margin")).toBeInTheDocument(); + }); + + test("renders advanced option toggle with correct props", () => { + render(); + + const toggle = screen.getByTestId("advanced-option-toggle"); + expect(toggle).toHaveTextContent("environments.surveys.summary.embed_mode"); + expect(toggle).toHaveTextContent("environments.surveys.summary.embed_mode_description"); + expect(screen.getByTestId("custom-container-class")).toHaveTextContent("p-0"); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteEmbedTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteEmbedTab.tsx new file mode 100644 index 0000000000..3480fe9c72 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteEmbedTab.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle"; +import { Button } from "@/modules/ui/components/button"; +import { CodeBlock } from "@/modules/ui/components/code-block"; +import { useTranslate } from "@tolgee/react"; +import { CopyIcon } from "lucide-react"; +import { useState } from "react"; +import toast from "react-hot-toast"; + +interface WebsiteEmbedTabProps { + surveyUrl: string; +} + +export const WebsiteEmbedTab = ({ surveyUrl }: WebsiteEmbedTabProps) => { + const [embedModeEnabled, setEmbedModeEnabled] = useState(false); + const { t } = useTranslate(); + + const iframeCode = `
+ +
`; + + return ( + <> +
+ + {iframeCode} + +
+ + + + ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.test.tsx deleted file mode 100644 index 9902d1bb3b..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.test.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import toast from "react-hot-toast"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { WebsiteTab } from "./WebsiteTab"; - -// Mock child components and hooks -const mockAdvancedOptionToggle = vi.fn(); -vi.mock("@/modules/ui/components/advanced-option-toggle", () => ({ - AdvancedOptionToggle: (props: any) => { - mockAdvancedOptionToggle(props); - return ( -
- {props.title} - props.onToggle(!props.isChecked)} /> -
- ); - }, -})); - -vi.mock("@/modules/ui/components/button", () => ({ - Button: ({ children, onClick, ...props }: any) => ( - - ), -})); - -const mockCodeBlock = vi.fn(); -vi.mock("@/modules/ui/components/code-block", () => ({ - CodeBlock: (props: any) => { - mockCodeBlock(props); - return ( -
- {props.children} -
- ); - }, -})); - -const mockOptionsSwitch = vi.fn(); -vi.mock("@/modules/ui/components/options-switch", () => ({ - OptionsSwitch: (props: any) => { - mockOptionsSwitch(props); - return ( -
- {props.options.map((opt: { value: string; label: string }) => ( - - ))} -
- ); - }, -})); - -vi.mock("lucide-react", () => ({ - CopyIcon: () =>
, -})); - -vi.mock("next/link", () => ({ - default: ({ children, href, target }: { children: React.ReactNode; href: string; target?: string }) => ( - - {children} - - ), -})); - -const mockWriteText = vi.fn(); -Object.defineProperty(navigator, "clipboard", { - value: { - writeText: mockWriteText, - }, - configurable: true, -}); - -const surveyUrl = "https://app.formbricks.com/s/survey123"; -const environmentId = "env456"; - -describe("WebsiteTab", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders OptionsSwitch and StaticTab by default", () => { - render(); - expect(screen.getByTestId("options-switch")).toBeInTheDocument(); - expect(mockOptionsSwitch).toHaveBeenCalledWith( - expect.objectContaining({ - currentOption: "static", - options: [ - { value: "static", label: "environments.surveys.summary.static_iframe" }, - { value: "popup", label: "environments.surveys.summary.dynamic_popup" }, - ], - }) - ); - // StaticTab content checks - expect(screen.getByText("common.copy_code")).toBeInTheDocument(); - expect(screen.getByTestId("code-block")).toBeInTheDocument(); - expect(screen.getByTestId("advanced-option-toggle")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.static_iframe")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.dynamic_popup")).toBeInTheDocument(); - }); - - test("switches to PopupTab when 'Dynamic Popup' option is clicked", async () => { - render(); - const popupButton = screen.getByRole("button", { - name: "environments.surveys.summary.dynamic_popup", - }); - await userEvent.click(popupButton); - - expect(mockOptionsSwitch.mock.calls.some((call) => call[0].currentOption === "popup")).toBe(true); - // PopupTab content checks - expect(screen.getByText("environments.surveys.summary.embed_pop_up_survey_title")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.setup_instructions")).toBeInTheDocument(); - expect(screen.getByRole("list")).toBeInTheDocument(); // Check for the ol element - - const listItems = screen.getAllByRole("listitem"); - expect(listItems[0]).toHaveTextContent( - "common.follow_these environments.surveys.summary.setup_instructions environments.surveys.summary.to_connect_your_website_with_formbricks" - ); - expect(listItems[1]).toHaveTextContent( - "environments.surveys.summary.make_sure_the_survey_type_is_set_to common.website_survey" - ); - expect(listItems[2]).toHaveTextContent( - "environments.surveys.summary.define_when_and_where_the_survey_should_pop_up" - ); - - expect( - screen.getByRole("link", { name: "environments.surveys.summary.setup_instructions" }) - ).toHaveAttribute("href", `/environments/${environmentId}/project/website-connection`); - expect( - screen.getByText("environments.surveys.summary.unsupported_video_tag_warning").closest("video") - ).toBeInTheDocument(); - }); - - describe("StaticTab", () => { - const formattedBaseCode = `
\n \n
`; - const normalizedBaseCode = `
`; - - const formattedEmbedCode = `
\n \n
`; - const normalizedEmbedCode = `
`; - - test("renders correctly with initial iframe code and embed mode toggle", () => { - render(); // Defaults to StaticTab - - expect(screen.getByTestId("code-block")).toHaveTextContent(normalizedBaseCode); - expect(mockCodeBlock).toHaveBeenCalledWith( - expect.objectContaining({ children: formattedBaseCode, language: "html" }) - ); - - expect(screen.getByTestId("advanced-option-toggle")).toBeInTheDocument(); - expect(mockAdvancedOptionToggle).toHaveBeenCalledWith( - expect.objectContaining({ - isChecked: false, - title: "environments.surveys.summary.embed_mode", - description: "environments.surveys.summary.embed_mode_description", - }) - ); - expect(screen.getByText("environments.surveys.summary.embed_mode")).toBeInTheDocument(); - }); - - test("copies iframe code to clipboard when 'Copy Code' is clicked", async () => { - render(); - const copyButton = screen.getByRole("button", { name: "Embed survey in your website" }); - - await userEvent.click(copyButton); - - expect(mockWriteText).toHaveBeenCalledWith(formattedBaseCode); - expect(toast.success).toHaveBeenCalledWith( - "environments.surveys.summary.embed_code_copied_to_clipboard" - ); - expect(screen.getByText("common.copy_code")).toBeInTheDocument(); - }); - - test("updates iframe code when 'Embed Mode' is toggled", async () => { - render(); - const embedToggle = screen - .getByTestId("advanced-option-toggle") - .querySelector('input[type="checkbox"]'); - expect(embedToggle).not.toBeNull(); - - await userEvent.click(embedToggle!); - - expect(screen.getByTestId("code-block")).toHaveTextContent(normalizedEmbedCode); - expect(mockCodeBlock.mock.calls.find((call) => call[0].children === formattedEmbedCode)).toBeTruthy(); - expect(mockAdvancedOptionToggle.mock.calls.some((call) => call[0].isChecked === true)).toBe(true); - - // Toggle back - await userEvent.click(embedToggle!); - expect(screen.getByTestId("code-block")).toHaveTextContent(normalizedBaseCode); - expect(mockCodeBlock.mock.calls.find((call) => call[0].children === formattedBaseCode)).toBeTruthy(); - expect(mockAdvancedOptionToggle.mock.calls.some((call) => call[0].isChecked === false)).toBe(true); - }); - }); - - describe("PopupTab", () => { - beforeEach(async () => { - // Ensure PopupTab is active - render(); - const popupButton = screen.getByRole("button", { - name: "environments.surveys.summary.dynamic_popup", - }); - await userEvent.click(popupButton); - }); - - test("renders title and instructions", () => { - expect(screen.getByText("environments.surveys.summary.embed_pop_up_survey_title")).toBeInTheDocument(); - - const listItems = screen.getAllByRole("listitem"); - expect(listItems).toHaveLength(3); - expect(listItems[0]).toHaveTextContent( - "common.follow_these environments.surveys.summary.setup_instructions environments.surveys.summary.to_connect_your_website_with_formbricks" - ); - expect(listItems[1]).toHaveTextContent( - "environments.surveys.summary.make_sure_the_survey_type_is_set_to common.website_survey" - ); - expect(listItems[2]).toHaveTextContent( - "environments.surveys.summary.define_when_and_where_the_survey_should_pop_up" - ); - - // Specific checks for elements or distinct text content - expect(screen.getByText("environments.surveys.summary.setup_instructions")).toBeInTheDocument(); // Checks the link text - expect(screen.getByText("common.website_survey")).toBeInTheDocument(); // Checks the bold text - // The text for the last list item is its sole content, so getByText works here. - expect( - screen.getByText("environments.surveys.summary.define_when_and_where_the_survey_should_pop_up") - ).toBeInTheDocument(); - }); - - test("renders the setup instructions link with correct href", () => { - const link = screen.getByRole("link", { name: "environments.surveys.summary.setup_instructions" }); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute("href", `/environments/${environmentId}/project/website-connection`); - expect(link).toHaveAttribute("target", "_blank"); - }); - - test("renders the video", () => { - const videoElement = screen - .getByText("environments.surveys.summary.unsupported_video_tag_warning") - .closest("video"); - expect(videoElement).toBeInTheDocument(); - expect(videoElement).toHaveAttribute("autoPlay"); - expect(videoElement).toHaveAttribute("loop"); - const sourceElement = videoElement?.querySelector("source"); - expect(sourceElement).toHaveAttribute("src", "/video/tooltips/change-survey-type.mp4"); - expect(sourceElement).toHaveAttribute("type", "video/mp4"); - expect( - screen.getByText("environments.surveys.summary.unsupported_video_tag_warning") - ).toBeInTheDocument(); - }); - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx deleted file mode 100644 index 535e4c1c7f..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; - -import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle"; -import { Button } from "@/modules/ui/components/button"; -import { CodeBlock } from "@/modules/ui/components/code-block"; -import { OptionsSwitch } from "@/modules/ui/components/options-switch"; -import { useTranslate } from "@tolgee/react"; -import { CopyIcon } from "lucide-react"; -import Link from "next/link"; -import { useState } from "react"; -import toast from "react-hot-toast"; - -export const WebsiteTab = ({ surveyUrl, environmentId }) => { - const [selectedTab, setSelectedTab] = useState("static"); - const { t } = useTranslate(); - - return ( -
- setSelectedTab(value)} - /> - -
- {selectedTab === "static" ? ( - - ) : ( - - )} -
-
- ); -}; - -const StaticTab = ({ surveyUrl }) => { - const [embedModeEnabled, setEmbedModeEnabled] = useState(false); - const { t } = useTranslate(); - const iframeCode = `
- -
`; - - return ( -
-
-
- -
-
- - {iframeCode} - -
-
- -
-
- ); -}; - -const PopupTab = ({ environmentId }) => { - const { t } = useTranslate(); - return ( -
-

- {t("environments.surveys.summary.embed_pop_up_survey_title")} -

-
    -
  1. - {t("common.follow_these")}{" "} - - {t("environments.surveys.summary.setup_instructions")} - {" "} - {t("environments.surveys.summary.to_connect_your_website_with_formbricks")} -
  2. -
  3. - {t("environments.surveys.summary.make_sure_the_survey_type_is_set_to")}{" "} - {t("common.website_survey")} -
  4. -
  5. {t("environments.surveys.summary.define_when_and_where_the_survey_should_pop_up")}
  6. -
-
- -
-
- ); -}; 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 1d7f764f1d..6c1a1145ba 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 @@ -22,16 +22,30 @@ vi.mock("./LinkTab", () => ({
), })); -vi.mock("./WebsiteTab", () => ({ - WebsiteTab: (props: { surveyUrl: string; environmentId: string }) => ( -
- WebsiteTab Content for {props.surveyUrl} in {props.environmentId} +vi.mock("./WebsiteEmbedTab", () => ({ + WebsiteEmbedTab: (props: { surveyUrl: string }) => ( +
WebsiteEmbedTab Content for {props.surveyUrl}
+ ), +})); +vi.mock("./DynamicPopupTab", () => ({ + DynamicPopupTab: (props: { environmentId: string; surveyId: string }) => ( +
+ DynamicPopupTab Content for {props.surveyId} in {props.environmentId} +
+ ), +})); +vi.mock("./TabContainer", () => ({ + TabContainer: (props: { children: React.ReactNode; title: string; description: string }) => ( +
+
{props.title}
+
{props.description}
+ {props.children}
), })); vi.mock("./personal-links-tab", () => ({ - PersonalLinksTab: (props: { segments: any[]; surveyId: string; environmentId: string }) => ( + PersonalLinksTab: (props: { surveyId: string; environmentId: string }) => (
PersonalLinksTab Content for {props.surveyId} in {props.environmentId}
@@ -39,7 +53,7 @@ vi.mock("./personal-links-tab", () => ({ })); vi.mock("@/modules/ui/components/upgrade-prompt", () => ({ - UpgradePrompt: (props: { title: string; description: string; buttons: any[] }) => ( + UpgradePrompt: (props: { title: string; description: string }) => (
{props.title} - {props.description}
@@ -53,6 +67,7 @@ vi.mock("lucide-react", () => ({ LinkIcon: () =>
LinkIcon
, GlobeIcon: () =>
GlobeIcon
, SmartphoneIcon: () =>
SmartphoneIcon
, + CheckCircle2Icon: () =>
CheckCircle2Icon
, AlertCircle: ({ className }: { className?: string }) => (
AlertCircle @@ -132,7 +147,8 @@ vi.mock("@/lib/cn", () => ({ const mockTabs = [ { id: "email", label: "Email", icon: () =>
}, - { id: "webpage", label: "Web Page", icon: () =>
}, + { id: "website-embed", label: "Website Embed", icon: () =>
}, + { id: "dynamic-popup", label: "Dynamic Popup", icon: () =>
}, { id: "link", label: "Link", icon: () =>
}, { id: "app", label: "App", icon: () =>
}, ]; @@ -268,9 +284,9 @@ describe("ShareView", () => { test("calls setActiveId when a tab is clicked (desktop)", async () => { render(); - const webpageTabButton = screen.getByLabelText("Web Page"); - await userEvent.click(webpageTabButton); - expect(defaultProps.setActiveId).toHaveBeenCalledWith("webpage"); + const websiteEmbedTabButton = screen.getByLabelText("Website Embed"); + await userEvent.click(websiteEmbedTabButton); + expect(defaultProps.setActiveId).toHaveBeenCalledWith("website-embed"); }); test("renders EmailTab when activeId is 'email'", () => { @@ -281,11 +297,21 @@ describe("ShareView", () => { ).toBeInTheDocument(); }); - test("renders WebsiteTab when activeId is 'webpage'", () => { - render(); - expect(screen.getByTestId("website-tab")).toBeInTheDocument(); + test("renders WebsiteEmbedTab when activeId is 'website-embed'", () => { + render(); + expect(screen.getByTestId("tab-container")).toBeInTheDocument(); + 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("tab-container")).toBeInTheDocument(); + expect(screen.getByTestId("dynamic-popup-tab")).toBeInTheDocument(); expect( - screen.getByText(`WebsiteTab Content for ${defaultProps.surveyUrl} in ${defaultProps.environmentId}`) + screen.getByText( + `DynamicPopupTab Content for ${defaultProps.survey.id} in ${defaultProps.environmentId}` + ) ).toBeInTheDocument(); }); @@ -316,7 +342,7 @@ describe("ShareView", () => { render(); // Get responsive buttons - these are Button components containing icons - const responsiveButtons = screen.getAllByTestId("webpage-tab-icon"); + const responsiveButtons = screen.getAllByTestId("website-embed-tab-icon"); // The responsive button should be the one inside the md:hidden container const responsiveButton = responsiveButtons .find((icon) => { @@ -327,7 +353,7 @@ describe("ShareView", () => { if (responsiveButton) { await userEvent.click(responsiveButton); - expect(defaultProps.setActiveId).toHaveBeenCalledWith("webpage"); + expect(defaultProps.setActiveId).toHaveBeenCalledWith("website-embed"); } }); @@ -339,9 +365,9 @@ describe("ShareView", () => { expect(emailTabButton).toHaveClass("font-medium"); expect(emailTabButton).toHaveClass("text-slate-900"); - const webpageTabButton = screen.getByLabelText("Web Page"); - expect(webpageTabButton).not.toHaveClass("bg-slate-100"); - expect(webpageTabButton).not.toHaveClass("font-medium"); + const websiteEmbedTabButton = screen.getByLabelText("Website Embed"); + expect(websiteEmbedTabButton).not.toHaveClass("bg-slate-100"); + expect(websiteEmbedTabButton).not.toHaveClass("font-medium"); }); test("applies active styles to the active tab (responsive)", () => { @@ -361,16 +387,18 @@ describe("ShareView", () => { expect(responsiveEmailButton).toHaveClass("bg-white text-slate-900 shadow-sm hover:bg-white"); } - const responsiveWebpageButtons = screen.getAllByTestId("webpage-tab-icon"); - const responsiveWebpageButton = responsiveWebpageButtons + const responsiveWebsiteEmbedButtons = screen.getAllByTestId("website-embed-tab-icon"); + const responsiveWebsiteEmbedButton = responsiveWebsiteEmbedButtons .find((icon) => { const button = icon.closest("button"); return button && button.getAttribute("data-variant") === "ghost"; }) ?.closest("button"); - if (responsiveWebpageButton) { - expect(responsiveWebpageButton).toHaveClass("border-transparent text-slate-700 hover:text-slate-900"); + if (responsiveWebsiteEmbedButton) { + expect(responsiveWebsiteEmbedButton).toHaveClass( + "border-transparent text-slate-700 hover:text-slate-900" + ); } }); }); 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 955e42c08b..8163c0ce23 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,5 +1,7 @@ "use client"; +import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/DynamicPopupTab"; +import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/TabContainer"; import { cn } from "@/lib/cn"; import { Button } from "@/modules/ui/components/button"; import { @@ -15,6 +17,7 @@ import { } from "@/modules/ui/components/sidebar"; 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"; @@ -22,7 +25,7 @@ import { TUserLocale } from "@formbricks/types/user"; import { AppTab } from "./AppTab"; import { EmailTab } from "./EmailTab"; import { LinkTab } from "./LinkTab"; -import { WebsiteTab } from "./WebsiteTab"; +import { WebsiteEmbedTab } from "./WebsiteEmbedTab"; import { PersonalLinksTab } from "./personal-links-tab"; interface ShareViewProps { @@ -57,6 +60,7 @@ export const ShareView = ({ isFormbricksCloud, }: ShareViewProps) => { const [isLargeScreen, setIsLargeScreen] = useState(true); + const { t } = useTranslate(); useEffect(() => { const checkScreenSize = () => { @@ -74,8 +78,22 @@ export const ShareView = ({ switch (activeId) { case "email": return ; - case "webpage": - return ; + case "website-embed": + return ( + + + + ); + case "dynamic-popup": + return ( + + + + ); case "link": return ( { const alertElement = screen.getByRole("alert"); expect(alertElement).toHaveClass("my-custom-class"); }); + + test("applies correct styles to anchor tags inside alert variants", () => { + render( + + Info Alert with Link + This alert has a link + + Test Link + + + ); + + const alertElement = screen.getByRole("alert"); + expect(alertElement).toHaveClass("text-info-foreground"); + expect(alertElement).toHaveClass("border-info/50"); + + const linkElement = screen.getByRole("link", { name: "Test Link" }); + expect(linkElement).toBeInTheDocument(); + }); + + test("applies correct styles to anchor tags in AlertButton with asChild", () => { + render( + + Error Alert + This alert has a button link + + Take Action + + + ); + + const alertElement = screen.getByRole("alert"); + expect(alertElement).toHaveClass("text-error-foreground"); + expect(alertElement).toHaveClass("border-error/50"); + + const linkElement = screen.getByRole("link", { name: "Take Action" }); + expect(linkElement).toBeInTheDocument(); + }); + + test("applies styles for all alert variants with anchor tags", () => { + const variants = ["error", "warning", "info", "success"] as const; + + variants.forEach((variant) => { + const { unmount } = render( + + {variant} Alert + Alert with anchor tag + + Link + + + ); + + const alertElement = screen.getByRole("alert"); + expect(alertElement).toHaveClass(`text-${variant}-foreground`); + expect(alertElement).toHaveClass(`border-${variant}/50`); + + const linkElement = screen.getByTestId(`${variant}-link`); + expect(linkElement).toBeInTheDocument(); + + unmount(); + }); + }); }); diff --git a/apps/web/modules/ui/components/alert/index.tsx b/apps/web/modules/ui/components/alert/index.tsx index 7f84e11968..b40ec6f95d 100644 --- a/apps/web/modules/ui/components/alert/index.tsx +++ b/apps/web/modules/ui/components/alert/index.tsx @@ -26,12 +26,12 @@ const alertVariants = cva("relative w-full rounded-lg border [&>svg]:size-4", { variant: { default: "text-foreground border-border", error: - "text-error-foreground [&>svg]:text-error border-error/50 [&_button]:bg-error-background [&_button]:text-error-foreground [&_button:hover]:bg-error-background-muted", + "text-error-foreground [&>svg]:text-error border-error/50 [&_button]:bg-error-background [&_button]:text-error-foreground [&_button:hover]:bg-error-background-muted [&_a]:bg-error-background [&_a]:text-error-foreground [&_a:hover]:bg-error-background-muted", warning: - "text-warning-foreground [&>svg]:text-warning border-warning/50 [&_button]:bg-warning-background [&_button]:text-warning-foreground [&_button:hover]:bg-warning-background-muted", - info: "text-info-foreground [&>svg]:text-info border-info/50 [&_button]:bg-info-background [&_button]:text-info-foreground [&_button:hover]:bg-info-background-muted", + "text-warning-foreground [&>svg]:text-warning border-warning/50 [&_button]:bg-warning-background [&_button]:text-warning-foreground [&_button:hover]:bg-warning-background-muted [&_a]:bg-warning-background [&_a]:text-warning-foreground [&_a:hover]:bg-warning-background-muted", + info: "text-info-foreground [&>svg]:text-info border-info/50 [&_button]:bg-info-background [&_button]:text-info-foreground [&_button:hover]:bg-info-background-muted [&_a]:bg-info-background [&_a]:text-info-foreground [&_a:hover]:bg-info-background-muted", success: - "text-success-foreground [&>svg]:text-success border-success/50 [&_button]:bg-success-background [&_button]:text-success-foreground [&_button:hover]:bg-success-background-muted", + "text-success-foreground [&>svg]:text-success border-success/50 [&_button]:bg-success-background [&_button]:text-success-foreground [&_button:hover]:bg-success-background-muted [&_a]:bg-success-background [&_a]:text-success-foreground [&_a:hover]:bg-success-background-muted", }, size: { default: diff --git a/apps/web/modules/ui/components/code-block/index.test.tsx b/apps/web/modules/ui/components/code-block/index.test.tsx index 13a7cb76e9..3f60eb73ab 100644 --- a/apps/web/modules/ui/components/code-block/index.test.tsx +++ b/apps/web/modules/ui/components/code-block/index.test.tsx @@ -118,4 +118,39 @@ describe("CodeBlock", () => { expect(codeElement).toHaveClass(`language-${language}`); expect(codeElement).toHaveClass(customCodeClass); }); + + test("applies no margin class when noMargin is true", () => { + const codeSnippet = "const test = 'no margin';"; + const language = "javascript"; + render( + + {codeSnippet} + + ); + + const containerElement = screen.getByText(codeSnippet).closest("div"); + expect(containerElement).not.toHaveClass("mt-4"); + }); + + test("applies default margin class when noMargin is false", () => { + const codeSnippet = "const test = 'with margin';"; + const language = "javascript"; + render( + + {codeSnippet} + + ); + + const containerElement = screen.getByText(codeSnippet).closest("div"); + expect(containerElement).toHaveClass("mt-4"); + }); + + test("applies default margin class when noMargin is undefined", () => { + const codeSnippet = "const test = 'default margin';"; + const language = "javascript"; + render({codeSnippet}); + + const containerElement = screen.getByText(codeSnippet).closest("div"); + expect(containerElement).toHaveClass("mt-4"); + }); }); diff --git a/apps/web/modules/ui/components/code-block/index.tsx b/apps/web/modules/ui/components/code-block/index.tsx index be1cefccf7..2be2420510 100644 --- a/apps/web/modules/ui/components/code-block/index.tsx +++ b/apps/web/modules/ui/components/code-block/index.tsx @@ -15,6 +15,7 @@ interface CodeBlockProps { customCodeClass?: string; customEditorClass?: string; showCopyToClipboard?: boolean; + noMargin?: boolean; } export const CodeBlock = ({ @@ -23,6 +24,7 @@ export const CodeBlock = ({ customEditorClass = "", customCodeClass = "", showCopyToClipboard = true, + noMargin = false, }: CodeBlockProps) => { const { t } = useTranslate(); useEffect(() => { @@ -30,7 +32,7 @@ export const CodeBlock = ({ }, [children]); return ( -
+
{showCopyToClipboard && (
{ 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-lg"); + expect(h3Element?.className).toContain("tracking-tight"); expect(h3Element?.className).toContain("text-slate-800"); }); @@ -49,8 +49,8 @@ describe("Typography Components", () => { 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-base"); + expect(h4Element?.className).toContain("tracking-tight"); expect(h4Element?.className).toContain("text-slate-800"); }); @@ -75,12 +75,11 @@ describe("Typography Components", () => { test("renders Large correctly", () => { const { container } = render(Large text); - const divElement = container.querySelector("div"); + const pElement = container.querySelector("p"); - expect(divElement).toBeInTheDocument(); - expect(divElement).toHaveTextContent("Large text"); - expect(divElement?.className).toContain("text-lg"); - expect(divElement?.className).toContain("font-semibold"); + expect(pElement).toBeInTheDocument(); + expect(pElement).toHaveTextContent("Large text"); + expect(pElement?.className).toContain("text-lg"); }); test("renders Small correctly", () => { @@ -90,6 +89,8 @@ describe("Typography Components", () => { expect(pElement).toBeInTheDocument(); expect(pElement).toHaveTextContent("Small text"); expect(pElement?.className).toContain("text-sm"); + expect(pElement?.className).toContain("leading-none"); + expect(pElement?.className).toContain("text-slate-800"); expect(pElement?.className).toContain("font-medium"); }); diff --git a/apps/web/modules/ui/components/typography/index.tsx b/apps/web/modules/ui/components/typography/index.tsx index e003fa89ac..f2170064d3 100644 --- a/apps/web/modules/ui/components/typography/index.tsx +++ b/apps/web/modules/ui/components/typography/index.tsx @@ -1,4 +1,5 @@ import { cn } from "@/modules/ui/lib/utils"; +import { cva } from "class-variance-authority"; import React, { forwardRef } from "react"; const H1 = forwardRef>((props, ref) => { @@ -40,7 +41,7 @@ const H3 = forwardRef + className={cn("scroll-m-20 text-lg tracking-tight text-slate-800", props.className)}> {props.children} ); @@ -54,7 +55,7 @@ const H4 = forwardRef + className={cn("scroll-m-20 text-base tracking-tight text-slate-800", props.className)}> {props.children} ); @@ -87,18 +88,53 @@ export { P }; const Large = forwardRef>((props, ref) => { return ( -
+

{props.children} -

+

); }); Large.displayName = "Large"; export { Large }; -const Small = forwardRef>((props, ref) => { +const Base = forwardRef>((props, ref) => { return ( -

+

+ {props.children} +

+ ); +}); + +Base.displayName = "Base"; +export { Base }; + +const smallVariants = cva("text-sm leading-none", { + variants: { + color: { + default: "text-slate-800 font-medium", + muted: "text-slate-500", + }, + margin: { + default: "mt-0", + headerDescription: "mt-1", + }, + }, +}); + +interface SmallProps extends React.HTMLAttributes { + color?: "default" | "muted"; + margin?: "default" | "headerDescription"; +} + +const Small = forwardRef((props, ref) => { + return ( +

{props.children}

); diff --git a/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp index 661e76177b..2c43fe71ac 100644 Binary files a/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp and b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp differ diff --git a/docs/xm-and-surveys/surveys/link-surveys/embed-surveys.mdx b/docs/xm-and-surveys/surveys/link-surveys/embed-surveys.mdx index 86dcdbba07..f731ba305f 100644 --- a/docs/xm-and-surveys/surveys/link-surveys/embed-surveys.mdx +++ b/docs/xm-and-surveys/surveys/link-surveys/embed-surveys.mdx @@ -79,7 +79,7 @@ Embed your survey with a minimalist design, disregarding padding and background. It can be enabled by simply appending **?embed=true** to your survey link or from UI -1. Open Embed survey tab in survey share modal +1. Open Website embed tab in survey share modal 2. Toggle **Embed mode**