feat: new share modal website embed and pop out (#6217)

This commit is contained in:
Victor Hugo dos Santos
2025-07-11 19:45:42 +07:00
committed by GitHub
parent 17d60eb1e7
commit 8af6c15998
27 changed files with 1011 additions and 441 deletions

View File

@@ -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}>
<div className="border-b border-slate-200 px-4 pb-4">
<div className="flex">
<h3 className="text-lg font-medium capitalize leading-6 text-slate-900">{title}</h3>
<H3 className="capitalize">{title}</H3>
<div className="ml-2">
{beta && <Badge size="normal" type="warning" text="Beta" />}
{soon && (
@@ -39,7 +40,9 @@ export const SettingsCard = ({
)}
</div>
</div>
<p className="mt-1 text-sm text-slate-500">{description}</p>
<Small color="muted" margin="headerDescription">
{description}
</Small>
</div>
<div className={cn(noPadding ? "" : "px-4 pt-4")}>{children}</div>
</div>

View File

@@ -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(<ShareSurveyModal {...defaultProps} survey={mockSurveyLink} modalView="share" />);
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(<ShareSurveyModal {...defaultProps} survey={mockSurveyWeb} modalView="share" />);
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(<ShareSurveyModal {...defaultProps} survey={mockSurveyLink} modalView="share" />);
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();
});
});

View File

@@ -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 (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="w-full bg-white p-0 lg:h-[700px]" width="wide">
<VisuallyHidden asChild>
<DialogTitle />
</VisuallyHidden>
<DialogContent className="w-full bg-white p-0 lg:h-[700px]" width="wide" aria-describedby={undefined}>
{showView === "start" ? (
<SuccessView
survey={survey}

View File

@@ -0,0 +1,178 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { DynamicPopupTab } from "./DynamicPopupTab";
// Mock components
vi.mock("@/modules/ui/components/alert", () => ({
Alert: (props: { variant?: string; size?: string; children: React.ReactNode }) => (
<div data-testid="alert" data-variant={props.variant} data-size={props.size}>
{props.children}
</div>
),
AlertButton: (props: { asChild?: boolean; children: React.ReactNode }) => (
<div data-testid="alert-button" data-as-child={props.asChild}>
{props.children}
</div>
),
AlertDescription: (props: { children: React.ReactNode }) => (
<div data-testid="alert-description">{props.children}</div>
),
AlertTitle: (props: { children: React.ReactNode }) => <div data-testid="alert-title">{props.children}</div>,
}));
vi.mock("@/modules/ui/components/button", () => ({
Button: (props: { variant?: string; asChild?: boolean; children: React.ReactNode }) => (
<div data-testid="button" data-variant={props.variant} data-as-child={props.asChild}>
{props.children}
</div>
),
}));
vi.mock("@/modules/ui/components/typography", () => ({
H4: (props: { children: React.ReactNode }) => <div data-testid="h4">{props.children}</div>,
}));
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key,
}),
}));
vi.mock("lucide-react", () => ({
ExternalLinkIcon: (props: { className?: string }) => (
<div data-testid="external-link-icon" className={props.className}>
ExternalLinkIcon
</div>
),
}));
// Mock Next.js Link
vi.mock("next/link", () => ({
default: (props: { href: string; target?: string; className?: string; children: React.ReactNode }) => (
<a href={props.href} target={props.target} className={props.className} data-testid="next-link">
{props.children}
</a>
),
}));
describe("DynamicPopupTab", () => {
afterEach(() => {
cleanup();
});
const defaultProps = {
environmentId: "env-123",
surveyId: "survey-123",
};
test("renders alert with correct props", () => {
render(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
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(<DynamicPopupTab {...defaultProps} />);
const documentationLinks = screen.getAllByTestId("next-link").slice(1, 4); // Skip the alert button link
documentationLinks.forEach((link) => {
expect(link).toHaveAttribute("target", "_blank");
});
});
});

View File

@@ -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 (
<Button variant="outline" asChild>
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-between p-4">
<div className="flex items-center gap-3">
<ExternalLinkIcon className="h-4 w-4 flex-shrink-0" />
<span className="text-left text-sm">{title}</span>
</div>
<span>{readDocsText}</span>
</Link>
</Button>
);
};
export const DynamicPopupTab = ({ environmentId, surveyId }: DynamicPopupTabProps) => {
const { t } = useTranslate();
return (
<div className="flex h-full flex-col justify-between space-y-4">
<Alert variant="info" size="default">
<AlertTitle>{t("environments.surveys.summary.dynamic_popup.alert_title")}</AlertTitle>
<AlertDescription>
{t("environments.surveys.summary.dynamic_popup.alert_description")}
</AlertDescription>
<AlertButton asChild>
<Link href={`/environments/${environmentId}/surveys/${surveyId}/edit`}>
{t("environments.surveys.summary.dynamic_popup.alert_button")}
</Link>
</AlertButton>
</Alert>
<div className="flex w-full flex-col space-y-4">
<H4>{t("environments.surveys.summary.dynamic_popup.title")}</H4>
<DocumentationButton
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting"
title={t("environments.surveys.summary.dynamic_popup.attribute_based_targeting")}
readDocsText={t("environments.surveys.summary.dynamic_popup.read_documentation")}
/>
<DocumentationButton
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions"
title={t("environments.surveys.summary.dynamic_popup.code_no_code_triggers")}
readDocsText={t("environments.surveys.summary.dynamic_popup.read_documentation")}
/>
<DocumentationButton
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/recontact"
title={t("environments.surveys.summary.dynamic_popup.recontact_options")}
readDocsText={t("environments.surveys.summary.dynamic_popup.read_documentation")}
/>
</div>
</div>
);
};

View File

@@ -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 }) => <h3 data-testid="h3">{props.children}</h3>,
Small: (props: { color?: string; margin?: string; children: React.ReactNode }) => (
<p data-testid="small" data-color={props.color} data-margin={props.margin}>
{props.children}
</p>
),
}));
describe("TabContainer", () => {
afterEach(() => {
cleanup();
});
const defaultProps = {
title: "Test Tab Title",
description: "Test tab description",
children: <div data-testid="tab-content">Tab content</div>,
};
test("renders title with correct props", () => {
render(<TabContainer {...defaultProps} />);
const title = screen.getByTestId("h3");
expect(title).toBeInTheDocument();
expect(title).toHaveTextContent("Test Tab Title");
});
test("renders description with correct text and props", () => {
render(<TabContainer {...defaultProps} />);
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(<TabContainer {...defaultProps} />);
const tabContent = screen.getByTestId("tab-content");
expect(tabContent).toBeInTheDocument();
expect(tabContent).toHaveTextContent("Tab content");
});
test("renders with correct container structure", () => {
render(<TabContainer {...defaultProps} />);
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(<TabContainer {...defaultProps} />);
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(<TabContainer {...defaultProps} />);
const container = screen.getByTestId("h3").parentElement?.parentElement;
expect(container).toContainElement(screen.getByTestId("tab-content"));
});
});

View File

@@ -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 (
<div className="flex h-full grow flex-col items-start space-y-4">
<div>
<H3>{title}</H3>
<Small color="muted" margin="headerDescription">
{description}
</Small>
</div>
{children}
</div>
);
};

View File

@@ -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;
}) => (
<div data-testid="advanced-option-toggle">
<label htmlFor={props.htmlId}>{props.title}</label>
<input
id={props.htmlId}
type="checkbox"
checked={props.isChecked}
onChange={(e) => props.onToggle(e.target.checked)}
data-testid="embed-mode-toggle"
/>
<span>{props.description}</span>
{props.customContainerClass && (
<span data-testid="custom-container-class">{props.customContainerClass}</span>
)}
</div>
),
}));
vi.mock("@/modules/ui/components/button", () => ({
Button: (props: {
title?: string;
"aria-label"?: string;
onClick?: () => void;
children: React.ReactNode;
type?: "button" | "submit" | "reset";
}) => (
<button
title={props.title}
aria-label={props["aria-label"]}
onClick={props.onClick}
data-testid="copy-button"
type={props.type}>
{props.children}
</button>
),
}));
vi.mock("@/modules/ui/components/code-block", () => ({
CodeBlock: (props: {
language: string;
showCopyToClipboard: boolean;
noMargin?: boolean;
children: string;
}) => (
<div data-testid="code-block">
<span data-testid="language">{props.language}</span>
<span data-testid="show-copy">{props.showCopyToClipboard.toString()}</span>
{props.noMargin && <span data-testid="no-margin">true</span>}
<pre>{props.children}</pre>
</div>
),
}));
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key,
}),
}));
vi.mock("lucide-react", () => ({
CopyIcon: () => <div data-testid="copy-icon">CopyIcon</div>,
}));
// 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(<WebsiteEmbedTab {...defaultProps} />);
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(<WebsiteEmbedTab {...defaultProps} />);
const codeBlock = screen.getByTestId("code-block");
expect(codeBlock).toBeInTheDocument();
const code = codeBlock.querySelector("pre")?.textContent;
expect(code).toContain(defaultProps.surveyUrl);
expect(code).toContain("<iframe");
expect(code).toContain('src="https://example.com/survey/123"');
expect(code).not.toContain("?embed=true");
});
test("renders correct iframe code with embed mode enabled", async () => {
render(<WebsiteEmbedTab {...defaultProps} />);
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(<WebsiteEmbedTab {...defaultProps} />);
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(<WebsiteEmbedTab {...defaultProps} />);
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(<WebsiteEmbedTab {...defaultProps} />);
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(<WebsiteEmbedTab {...defaultProps} />);
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(<WebsiteEmbedTab {...defaultProps} />);
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");
});
});

View File

@@ -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 = `<div style="position: relative; height:80dvh; overflow:auto;">
<iframe
src="${surveyUrl}${embedModeEnabled ? "?embed=true" : ""}"
frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;">
</iframe>
</div>`;
return (
<>
<div className="prose prose-slate max-w-full">
<CodeBlock
customCodeClass="text-sm h-48 overflow-y-scroll"
language="html"
showCopyToClipboard={false}
noMargin>
{iframeCode}
</CodeBlock>
</div>
<AdvancedOptionToggle
htmlId="enableEmbedMode"
isChecked={embedModeEnabled}
onToggle={setEmbedModeEnabled}
title={t("environments.surveys.summary.embed_mode")}
description={t("environments.surveys.summary.embed_mode_description")}
customContainerClass="p-0"
/>
<Button
title={t("common.copy_code")}
aria-label={t("common.copy_code")}
onClick={() => {
navigator.clipboard.writeText(iframeCode);
toast.success(t("environments.surveys.summary.embed_code_copied_to_clipboard"));
}}>
{t("common.copy_code")}
<CopyIcon />
</Button>
</>
);
};

View File

@@ -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 (
<div data-testid="advanced-option-toggle">
<span>{props.title}</span>
<input type="checkbox" checked={props.isChecked} onChange={() => props.onToggle(!props.isChecked)} />
</div>
);
},
}));
vi.mock("@/modules/ui/components/button", () => ({
Button: ({ children, onClick, ...props }: any) => (
<button onClick={onClick} {...props}>
{children}
</button>
),
}));
const mockCodeBlock = vi.fn();
vi.mock("@/modules/ui/components/code-block", () => ({
CodeBlock: (props: any) => {
mockCodeBlock(props);
return (
<div data-testid="code-block" data-language={props.language}>
{props.children}
</div>
);
},
}));
const mockOptionsSwitch = vi.fn();
vi.mock("@/modules/ui/components/options-switch", () => ({
OptionsSwitch: (props: any) => {
mockOptionsSwitch(props);
return (
<div data-testid="options-switch">
{props.options.map((opt: { value: string; label: string }) => (
<button key={opt.value} onClick={() => props.handleOptionChange(opt.value)}>
{opt.label}
</button>
))}
</div>
);
},
}));
vi.mock("lucide-react", () => ({
CopyIcon: () => <div data-testid="copy-icon" />,
}));
vi.mock("next/link", () => ({
default: ({ children, href, target }: { children: React.ReactNode; href: string; target?: string }) => (
<a href={href} target={target} data-testid="next-link">
{children}
</a>
),
}));
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(<WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />);
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(<WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />);
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 = `<div style="position: relative; height:80dvh; overflow:auto;"> \n <iframe \n src="${surveyUrl}" \n frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;">\n </iframe>\n</div>`;
const normalizedBaseCode = `<div style="position: relative; height:80dvh; overflow:auto;"> <iframe src="${surveyUrl}" frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;"> </iframe> </div>`;
const formattedEmbedCode = `<div style="position: relative; height:80dvh; overflow:auto;"> \n <iframe \n src="${surveyUrl}?embed=true" \n frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;">\n </iframe>\n</div>`;
const normalizedEmbedCode = `<div style="position: relative; height:80dvh; overflow:auto;"> <iframe src="${surveyUrl}?embed=true" frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;"> </iframe> </div>`;
test("renders correctly with initial iframe code and embed mode toggle", () => {
render(<WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />); // 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(<WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />);
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(<WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />);
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(<WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />);
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();
});
});
});

View File

@@ -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 (
<div className="flex h-full grow flex-col">
<OptionsSwitch
options={[
{ value: "static", label: t("environments.surveys.summary.static_iframe") },
{ value: "popup", label: t("environments.surveys.summary.dynamic_popup") },
]}
currentOption={selectedTab}
handleOptionChange={(value) => setSelectedTab(value)}
/>
<div className="mt-4">
{selectedTab === "static" ? (
<StaticTab surveyUrl={surveyUrl} />
) : (
<PopupTab environmentId={environmentId} />
)}
</div>
</div>
);
};
const StaticTab = ({ surveyUrl }) => {
const [embedModeEnabled, setEmbedModeEnabled] = useState(false);
const { t } = useTranslate();
const iframeCode = `<div style="position: relative; height:80dvh; overflow:auto;">
<iframe
src="${surveyUrl}${embedModeEnabled ? "?embed=true" : ""}"
frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;">
</iframe>
</div>`;
return (
<div className="flex h-full grow flex-col">
<div className="flex justify-between">
<div></div>
<Button
title="Embed survey in your website"
aria-label="Embed survey in your website"
onClick={() => {
navigator.clipboard.writeText(iframeCode);
toast.success(t("environments.surveys.summary.embed_code_copied_to_clipboard"));
}}>
{t("common.copy_code")}
<CopyIcon />
</Button>
</div>
<div className="prose prose-slate max-w-full">
<CodeBlock
customCodeClass="text-sm h-48 overflow-y-scroll text-sm"
language="html"
showCopyToClipboard={false}>
{iframeCode}
</CodeBlock>
</div>
<div className="mt-2 rounded-md border bg-white p-4">
<AdvancedOptionToggle
htmlId="enableEmbedMode"
isChecked={embedModeEnabled}
onToggle={setEmbedModeEnabled}
title={t("environments.surveys.summary.embed_mode")}
description={t("environments.surveys.summary.embed_mode_description")}
childBorder={true}
/>
</div>
</div>
);
};
const PopupTab = ({ environmentId }) => {
const { t } = useTranslate();
return (
<div>
<p className="text-lg font-semibold text-slate-800">
{t("environments.surveys.summary.embed_pop_up_survey_title")}
</p>
<ol className="mt-4 list-decimal space-y-2 pl-5 text-sm text-slate-700">
<li>
{t("common.follow_these")}{" "}
<Link
href={`/environments/${environmentId}/project/website-connection`}
target="_blank"
className="decoration-brand-dark font-medium underline underline-offset-2">
{t("environments.surveys.summary.setup_instructions")}
</Link>{" "}
{t("environments.surveys.summary.to_connect_your_website_with_formbricks")}
</li>
<li>
{t("environments.surveys.summary.make_sure_the_survey_type_is_set_to")}{" "}
<b>{t("common.website_survey")}</b>
</li>
<li>{t("environments.surveys.summary.define_when_and_where_the_survey_should_pop_up")}</li>
</ol>
<div className="mt-4">
<video autoPlay loop muted className="w-full rounded-xl border border-slate-200">
<source src="/video/tooltips/change-survey-type.mp4" type="video/mp4" />
{t("environments.surveys.summary.unsupported_video_tag_warning")}
</video>
</div>
</div>
);
};

View File

@@ -22,16 +22,30 @@ vi.mock("./LinkTab", () => ({
</div>
),
}));
vi.mock("./WebsiteTab", () => ({
WebsiteTab: (props: { surveyUrl: string; environmentId: string }) => (
<div data-testid="website-tab">
WebsiteTab Content for {props.surveyUrl} in {props.environmentId}
vi.mock("./WebsiteEmbedTab", () => ({
WebsiteEmbedTab: (props: { surveyUrl: string }) => (
<div data-testid="website-embed-tab">WebsiteEmbedTab Content for {props.surveyUrl}</div>
),
}));
vi.mock("./DynamicPopupTab", () => ({
DynamicPopupTab: (props: { environmentId: string; surveyId: string }) => (
<div data-testid="dynamic-popup-tab">
DynamicPopupTab Content for {props.surveyId} in {props.environmentId}
</div>
),
}));
vi.mock("./TabContainer", () => ({
TabContainer: (props: { children: React.ReactNode; title: string; description: string }) => (
<div data-testid="tab-container">
<div data-testid="tab-title">{props.title}</div>
<div data-testid="tab-description">{props.description}</div>
{props.children}
</div>
),
}));
vi.mock("./personal-links-tab", () => ({
PersonalLinksTab: (props: { segments: any[]; surveyId: string; environmentId: string }) => (
PersonalLinksTab: (props: { surveyId: string; environmentId: string }) => (
<div data-testid="personal-links-tab">
PersonalLinksTab Content for {props.surveyId} in {props.environmentId}
</div>
@@ -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 }) => (
<div data-testid="upgrade-prompt">
{props.title} - {props.description}
</div>
@@ -53,6 +67,7 @@ vi.mock("lucide-react", () => ({
LinkIcon: () => <div data-testid="link-icon">LinkIcon</div>,
GlobeIcon: () => <div data-testid="globe-icon">GlobeIcon</div>,
SmartphoneIcon: () => <div data-testid="smartphone-icon">SmartphoneIcon</div>,
CheckCircle2Icon: () => <div data-testid="check-circle-2-icon">CheckCircle2Icon</div>,
AlertCircle: ({ className }: { className?: string }) => (
<div className={className} data-testid="alert-circle">
AlertCircle
@@ -132,7 +147,8 @@ vi.mock("@/lib/cn", () => ({
const mockTabs = [
{ id: "email", label: "Email", icon: () => <div data-testid="email-tab-icon" /> },
{ id: "webpage", label: "Web Page", icon: () => <div data-testid="webpage-tab-icon" /> },
{ id: "website-embed", label: "Website Embed", icon: () => <div data-testid="website-embed-tab-icon" /> },
{ id: "dynamic-popup", label: "Dynamic Popup", icon: () => <div data-testid="dynamic-popup-tab-icon" /> },
{ id: "link", label: "Link", icon: () => <div data-testid="link-tab-icon" /> },
{ id: "app", label: "App", icon: () => <div data-testid="app-tab-icon" /> },
];
@@ -268,9 +284,9 @@ describe("ShareView", () => {
test("calls setActiveId when a tab is clicked (desktop)", async () => {
render(<ShareView {...defaultProps} survey={mockSurveyLink} activeId="email" />);
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(<ShareView {...defaultProps} activeId="webpage" />);
expect(screen.getByTestId("website-tab")).toBeInTheDocument();
test("renders WebsiteEmbedTab when activeId is 'website-embed'", () => {
render(<ShareView {...defaultProps} activeId="website-embed" />);
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(<ShareView {...defaultProps} activeId="dynamic-popup" />);
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(<ShareView {...defaultProps} survey={mockSurveyLink} activeId="email" />);
// 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"
);
}
});
});

View File

@@ -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 <EmailTab surveyId={survey.id} email={email} />;
case "webpage":
return <WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />;
case "website-embed":
return (
<TabContainer
title={t("environments.surveys.share.embed_on_website.title")}
description={t("environments.surveys.share.embed_on_website.description")}>
<WebsiteEmbedTab surveyUrl={surveyUrl} />
</TabContainer>
);
case "dynamic-popup":
return (
<TabContainer
title={t("environments.surveys.share.dynamic_popup.title")}
description={t("environments.surveys.share.dynamic_popup.description")}>
<DynamicPopupTab environmentId={environmentId} surveyId={survey.id} />
</TabContainer>
);
case "link":
return (
<LinkTab

View File

@@ -1699,6 +1699,16 @@
},
"results_unpublished_successfully": "Ergebnisse wurden nicht erfolgreich veröffentlicht.",
"search_by_survey_name": "Nach Umfragenamen suchen",
"share": {
"dynamic_popup": {
"description": "Formbricks Umfragen können als Pop-up eingebettet werden, basierend auf der Benutzerinteraktion.",
"title": "Nutzer im Ablauf abfangen, um kontextualisiertes Feedback zu sammeln"
},
"embed_on_website": {
"description": "Formbricks-Umfragen können als statisches Element eingebettet werden.",
"title": "Binden Sie die Umfrage auf Ihrer Webseite ein"
}
},
"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",
"added_filter_for_responses_where_answer_to_question_is_skipped": "Filter hinzugefügt für Antworten, bei denen die Frage {questionIdx} übersprungen wurde",
@@ -1723,6 +1733,17 @@
"drop_offs": "Drop-Off Rate",
"drop_offs_tooltip": "So oft wurde die Umfrage gestartet, aber nicht abgeschlossen.",
"dynamic_popup": "Dynamisch (Pop-up)",
"dynamic_popup.alert_button": "Umfrage bearbeiten",
"dynamic_popup.alert_description": "Diese Umfrage ist derzeit als Link-Umfrage konfiguriert, die dynamische Pop-ups nicht unterstützt. Sie können dies im Tab Einstellungen im Umfrage-Editor ändern.",
"dynamic_popup.alert_title": "Umfragen-Typ in In-App ändern",
"dynamic_popup.attribubte_description": "Attributbasiertes Targeting",
"dynamic_popup.attribute_based_targeting": "Attributbasiertes Targeting",
"dynamic_popup.code_no_code_description": "Code- und No-Code-Auslöser",
"dynamic_popup.code_no_code_triggers": "Code- und No-Code-Auslöser",
"dynamic_popup.read_documentation": "Dokumentation lesen",
"dynamic_popup.recontact_options": "Optionen zur erneuten Kontaktaufnahme",
"dynamic_popup.recontact_options_description": "Optionen zur erneuten Kontaktaufnahme",
"dynamic_popup.title": "Mehr mit Zwischenumfragen tun",
"email_sent": "E-Mail gesendet!",
"embed_code_copied_to_clipboard": "Einbettungscode in die Zwischenablage kopiert!",
"embed_in_an_email": "In eine E-Mail einbetten",
@@ -1731,7 +1752,6 @@
"embed_mode_description": "Bette deine Umfrage mit einem minimalistischen Design ein, ohne Karten und Hintergrund.",
"embed_on_website": "Auf Website einbetten",
"embed_pop_up_survey_title": "Wie man eine Pop-up-Umfrage auf seiner Website einbindet",
"embed_survey": "Umfrage einbetten",
"expiry_date_description": "Sobald der Link abläuft, kann der Empfänger nicht mehr auf die Umfrage antworten.",
"expiry_date_optional": "Ablaufdatum (optional)",
"failed_to_copy_link": "Kopieren des Links fehlgeschlagen",
@@ -1785,7 +1805,6 @@
"selected_responses_csv": "Ausgewählte Antworten (CSV)",
"selected_responses_excel": "Ausgewählte Antworten (Excel)",
"send_preview": "Vorschau senden",
"send_to_panel": "An das Panel senden",
"setup_instructions": "Einrichtung",
"setup_integrations": "Integrationen einrichten",
"share_results": "Ergebnisse teilen",

View File

@@ -1699,6 +1699,16 @@
},
"results_unpublished_successfully": "Results unpublished successfully.",
"search_by_survey_name": "Search by survey name",
"share": {
"dynamic_popup": {
"description": "Formbricks surveys can be embedded as a pop up, based on user interaction.",
"title": "Intercept users in their flow to gather contextualized feedback"
},
"embed_on_website": {
"description": "Formbricks surveys can be embedded as a static element.",
"title": "Embed the survey in your webpage"
}
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "Added filter for responses where answer to question {questionIdx} is {filterComboBoxValue} - {filterValue} ",
"added_filter_for_responses_where_answer_to_question_is_skipped": "Added filter for responses where answer to question {questionIdx} is skipped",
@@ -1723,15 +1733,25 @@
"drop_offs": "Drop-Offs",
"drop_offs_tooltip": "Number of times the survey has been started but not completed.",
"dynamic_popup": "Dynamic (Pop-up)",
"dynamic_popup.alert_button": "Edit survey",
"dynamic_popup.alert_description": "This survey is currently configured as a link survey, which does not support dynamic pop-ups. You can change this in the settings tab of the survey editor.",
"dynamic_popup.alert_title": "Change survey type to in-app",
"dynamic_popup.attribubte_description": "Attribute-based targeting",
"dynamic_popup.attribute_based_targeting": "Attribute-based targeting",
"dynamic_popup.code_no_code_description": "Code and no code triggers",
"dynamic_popup.code_no_code_triggers": "Code and no code triggers",
"dynamic_popup.read_documentation": "Read docs",
"dynamic_popup.recontact_options": "Recontact options",
"dynamic_popup.recontact_options_description": "Recontact options",
"dynamic_popup.title": "Do more with intercept surveys",
"email_sent": "Email sent!",
"embed_code_copied_to_clipboard": "Embed code copied to clipboard!",
"embed_in_an_email": "Embed in an email",
"embed_in_app": "Embed in app",
"embed_mode": "Embed Mode",
"embed_mode_description": "Embed your survey with a minimalist design, discarding padding and background.",
"embed_on_website": "Embed on website",
"embed_on_website": "Website embed",
"embed_pop_up_survey_title": "How to embed a pop-up survey on your website",
"embed_survey": "Embed survey",
"expiry_date_description": "Once the link expires, the recipient cannot respond to survey any longer.",
"expiry_date_optional": "Expiry date (optional)",
"failed_to_copy_link": "Failed to copy link",
@@ -1785,7 +1805,6 @@
"selected_responses_csv": "Selected responses (CSV)",
"selected_responses_excel": "Selected responses (Excel)",
"send_preview": "Send preview",
"send_to_panel": "Send to panel",
"setup_instructions": "Setup instructions",
"setup_integrations": "Setup integrations",
"share_results": "Share results",

View File

@@ -1699,6 +1699,16 @@
},
"results_unpublished_successfully": "Résultats publiés avec succès.",
"search_by_survey_name": "Recherche par nom d'enquête",
"share": {
"dynamic_popup": {
"description": "Les enquêtes Formbricks peuvent être intégrées sous forme de pop-up, en fonction de l'interaction de l'utilisateur.",
"title": "Interceptez les utilisateurs dans leur flux pour recueillir des retours contextualisés"
},
"embed_on_website": {
"description": "Les enquêtes Formbricks peuvent être intégrées comme élément statique.",
"title": "Intégrez le sondage sur votre page web"
}
},
"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'}' ",
"added_filter_for_responses_where_answer_to_question_is_skipped": "Filtre ajouté pour les réponses où la réponse à la question '{'questionIdx'}' est ignorée",
@@ -1723,6 +1733,17 @@
"drop_offs": "Dépôts",
"drop_offs_tooltip": "Nombre de fois que l'enquête a été commencée mais non terminée.",
"dynamic_popup": "Dynamique (Pop-up)",
"dynamic_popup.alert_button": "Modifier enquête",
"dynamic_popup.alert_description": "Ce sondage est actuellement configuré comme un sondage de lien, qui ne prend pas en charge les pop-ups dynamiques. Vous pouvez le modifier dans l'onglet des paramètres de l'éditeur de sondage.",
"dynamic_popup.alert_title": "Changer le type d'enquête en application intégrée",
"dynamic_popup.attribubte_description": "Ciblage basé sur des attributs",
"dynamic_popup.attribute_based_targeting": "Ciblage basé sur des attributs",
"dynamic_popup.code_no_code_description": "Déclencheurs avec et sans code",
"dynamic_popup.code_no_code_triggers": "Déclencheurs avec et sans code",
"dynamic_popup.read_documentation": "Lire les documents",
"dynamic_popup.recontact_options": "Options de recontact",
"dynamic_popup.recontact_options_description": "Options de recontact",
"dynamic_popup.title": "Faites plus avec les enquêtes d'interception",
"email_sent": "Email envoyé !",
"embed_code_copied_to_clipboard": "Code d'intégration copié dans le presse-papiers !",
"embed_in_an_email": "Inclure dans un e-mail",
@@ -1731,7 +1752,6 @@
"embed_mode_description": "Intégrez votre enquête avec un design minimaliste, en supprimant les marges et l'arrière-plan.",
"embed_on_website": "Incorporer sur le site web",
"embed_pop_up_survey_title": "Comment intégrer une enquête pop-up sur votre site web",
"embed_survey": "Intégrer l'enquête",
"expiry_date_description": "Une fois le lien expiré, le destinataire ne peut plus répondre au sondage.",
"expiry_date_optional": "Date d'expiration (facultatif)",
"failed_to_copy_link": "Échec de la copie du lien",
@@ -1785,7 +1805,6 @@
"selected_responses_csv": "Réponses sélectionnées (CSV)",
"selected_responses_excel": "Réponses sélectionnées (Excel)",
"send_preview": "Envoyer un aperçu",
"send_to_panel": "Envoyer au panneau",
"setup_instructions": "Instructions d'installation",
"setup_integrations": "Configurer les intégrations",
"share_results": "Partager les résultats",

View File

@@ -1699,6 +1699,16 @@
},
"results_unpublished_successfully": "Resultados não publicados com sucesso.",
"search_by_survey_name": "Buscar pelo nome da pesquisa",
"share": {
"dynamic_popup": {
"description": "\"As pesquisas do Formbricks podem ser integradas como um pop-up, baseado na interação do usuário.\"",
"title": "Intercepte os usuários em seu fluxo para coletar feedback contextualizado"
},
"embed_on_website": {
"description": "Os formulários Formbricks podem ser incorporados como um elemento estático.",
"title": "Incorporar a pesquisa na sua página da web"
}
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ",
"added_filter_for_responses_where_answer_to_question_is_skipped": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} foi pulada",
@@ -1723,6 +1733,17 @@
"drop_offs": "Pontos de Entrega",
"drop_offs_tooltip": "Número de vezes que a pesquisa foi iniciada mas não concluída.",
"dynamic_popup": "Dinâmico (Pop-up)",
"dynamic_popup.alert_button": "Editar pesquisa",
"dynamic_popup.alert_description": "Esta pesquisa está atualmente configurada como uma pesquisa de link, o que não suporta pop-ups dinâmicos. Você pode alterar isso na aba de configurações do editor de pesquisas.",
"dynamic_popup.alert_title": "Alterar o tipo de pesquisa para dentro do app",
"dynamic_popup.attribubte_description": "Segmentação baseada em atributos",
"dynamic_popup.attribute_based_targeting": "Segmentação baseada em atributos",
"dynamic_popup.code_no_code_description": "Gatilhos de código e sem código",
"dynamic_popup.code_no_code_triggers": "Gatilhos de código e sem código",
"dynamic_popup.read_documentation": "Leia Documentação",
"dynamic_popup.recontact_options": "Opções de Recontato",
"dynamic_popup.recontact_options_description": "Opções de Recontato",
"dynamic_popup.title": "Faça mais com pesquisas de interceptação",
"email_sent": "Email enviado!",
"embed_code_copied_to_clipboard": "Código incorporado copiado para a área de transferência!",
"embed_in_an_email": "Incorporar em um e-mail",
@@ -1731,7 +1752,6 @@
"embed_mode_description": "Incorpore sua pesquisa com um design minimalista, sem preenchimento e fundo.",
"embed_on_website": "Incorporar no site",
"embed_pop_up_survey_title": "Como incorporar uma pesquisa pop-up no seu site",
"embed_survey": "Incorporar pesquisa",
"expiry_date_description": "Quando o link expirar, o destinatário não poderá mais responder à pesquisa.",
"expiry_date_optional": "Data de expiração (opcional)",
"failed_to_copy_link": "Falha ao copiar link",
@@ -1785,7 +1805,6 @@
"selected_responses_csv": "Respostas selecionadas (CSV)",
"selected_responses_excel": "Respostas selecionadas (Excel)",
"send_preview": "Enviar prévia",
"send_to_panel": "Enviar para o painel",
"setup_instructions": "Instruções de configuração",
"setup_integrations": "Configurar integrações",
"share_results": "Compartilhar resultados",

View File

@@ -1699,6 +1699,16 @@
},
"results_unpublished_successfully": "Resultados despublicados com sucesso.",
"search_by_survey_name": "Pesquisar por nome do inquérito",
"share": {
"dynamic_popup": {
"description": "Os inquéritos Formbricks podem ser incorporados como uma janela pop-up, com base na interação do utilizador.",
"title": "Intercepte utilizadores no seu fluxo para recolher feedback contextualizado"
},
"embed_on_website": {
"description": "Os inquéritos Formbricks podem ser incorporados como um elemento estático.",
"title": "Incorporar o questionário na sua página web"
}
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ",
"added_filter_for_responses_where_answer_to_question_is_skipped": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é ignorada",
@@ -1723,6 +1733,17 @@
"drop_offs": "Desistências",
"drop_offs_tooltip": "Número de vezes que o inquérito foi iniciado mas não concluído.",
"dynamic_popup": "Dinâmico (Pop-up)",
"dynamic_popup.alert_button": "Editar inquérito",
"dynamic_popup.alert_description": "Este questionário está atualmente configurado como um questionário de link, que não suporta pop-ups dinâmicos. Você pode alterar isso na aba de configurações do editor de questionários.",
"dynamic_popup.alert_title": "Mudar tipo de inquérito para in-app",
"dynamic_popup.attribubte_description": "Segmentação baseada em atributos",
"dynamic_popup.attribute_based_targeting": "Segmentação baseada em atributos",
"dynamic_popup.code_no_code_description": "Gatilhos com código e sem código",
"dynamic_popup.code_no_code_triggers": "Gatilhos com código e sem código",
"dynamic_popup.read_documentation": "Ler Documentação",
"dynamic_popup.recontact_options": "Opções de Recontacto",
"dynamic_popup.recontact_options_description": "Opções de Recontacto",
"dynamic_popup.title": "Faça mais com sondagens de interceptação",
"email_sent": "Email enviado!",
"embed_code_copied_to_clipboard": "Código incorporado copiado para a área de transferência!",
"embed_in_an_email": "Incorporar num email",
@@ -1731,7 +1752,6 @@
"embed_mode_description": "Incorpore o seu inquérito com um design minimalista, descartando o preenchimento e o fundo.",
"embed_on_website": "Incorporar no site",
"embed_pop_up_survey_title": "Como incorporar um questionário pop-up no seu site",
"embed_survey": "Incorporar inquérito",
"expiry_date_description": "Uma vez que o link expira, o destinatário não pode mais responder ao questionário.",
"expiry_date_optional": "Data de expiração (opcional)",
"failed_to_copy_link": "Falha ao copiar link",
@@ -1785,7 +1805,6 @@
"selected_responses_csv": "Respostas selecionadas (CSV)",
"selected_responses_excel": "Respostas selecionadas (Excel)",
"send_preview": "Enviar pré-visualização",
"send_to_panel": "Enviar para painel",
"setup_instructions": "Instruções de configuração",
"setup_integrations": "Configurar integrações",
"share_results": "Partilhar resultados",

View File

@@ -1699,6 +1699,16 @@
},
"results_unpublished_successfully": "結果已成功取消發布。",
"search_by_survey_name": "依問卷名稱搜尋",
"share": {
"dynamic_popup": {
"description": "Formbricks 調查 可以 嵌入 為 彈出 式 樣 式 根據 使用者 互動 。",
"title": "攔截使用者於其流程中以收集具上下文的意見反饋"
},
"embed_on_website": {
"description": "Formbricks 調查可以 作為 靜態 元素 嵌入。",
"title": "嵌入 調查 在 您 的 網頁"
}
},
"summary": {
"added_filter_for_responses_where_answer_to_question": "已新增回應的篩選器,其中問題 '{'questionIdx'}' 的答案為 '{'filterComboBoxValue'}' - '{'filterValue'}'",
"added_filter_for_responses_where_answer_to_question_is_skipped": "已新增回應的篩選器,其中問題 '{'questionIdx'}' 的答案被跳過",
@@ -1723,6 +1733,17 @@
"drop_offs": "放棄",
"drop_offs_tooltip": "問卷已開始但未完成的次數。",
"dynamic_popup": "動態(彈窗)",
"dynamic_popup.alert_button": "編輯 問卷",
"dynamic_popup.alert_description": "此 問卷 目前 被 設定 為 連結 問卷,不 支援 動態 彈出窗口。您 可 在 問卷 編輯器 的 設定 標籤 中 進行 更改。",
"dynamic_popup.alert_title": "更改問卷類型為 in-app",
"dynamic_popup.attribubte_description": "屬性 基於 的 定位",
"dynamic_popup.attribute_based_targeting": "屬性 基於 的 定位",
"dynamic_popup.code_no_code_description": "程式碼 及 無程式碼 觸發器",
"dynamic_popup.code_no_code_triggers": "程式碼 及 無程式碼 觸發器",
"dynamic_popup.read_documentation": "閱讀 文件",
"dynamic_popup.recontact_options": "重新聯絡選項",
"dynamic_popup.recontact_options_description": "重新聯絡選項",
"dynamic_popup.title": "使用 截圖 調查 來 完成 更多 工作",
"email_sent": "已發送電子郵件!",
"embed_code_copied_to_clipboard": "嵌入程式碼已複製到剪貼簿!",
"embed_in_an_email": "嵌入電子郵件中",
@@ -1731,7 +1752,6 @@
"embed_mode_description": "以簡約設計嵌入您的問卷,捨棄邊距和背景。",
"embed_on_website": "嵌入網站",
"embed_pop_up_survey_title": "如何在您的網站上嵌入彈出式問卷",
"embed_survey": "嵌入問卷",
"expiry_date_description": "一旦連結過期,收件者將無法再回應 survey。",
"expiry_date_optional": "到期日 (可選)",
"failed_to_copy_link": "無法複製連結",
@@ -1785,7 +1805,6 @@
"selected_responses_csv": "選擇的回應 (CSV)",
"selected_responses_excel": "選擇的回應 (Excel)",
"send_preview": "發送預覽",
"send_to_panel": "發送到小組",
"setup_instructions": "設定說明",
"setup_integrations": "設定整合",
"share_results": "分享結果",

View File

@@ -1,3 +1,4 @@
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";
@@ -132,4 +133,67 @@ describe("Alert Component", () => {
const alertElement = screen.getByRole("alert");
expect(alertElement).toHaveClass("my-custom-class");
});
test("applies correct styles to anchor tags inside alert variants", () => {
render(
<Alert variant="info">
<AlertTitle>Info Alert with Link</AlertTitle>
<AlertDescription>This alert has a link</AlertDescription>
<a href="/test" className="test-link">
Test Link
</a>
</Alert>
);
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(
<Alert variant="error">
<AlertTitle>Error Alert</AlertTitle>
<AlertDescription>This alert has a button link</AlertDescription>
<AlertButton asChild>
<a href="/error-action">Take Action</a>
</AlertButton>
</Alert>
);
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(
<Alert variant={variant}>
<AlertTitle>{variant} Alert</AlertTitle>
<AlertDescription>Alert with anchor tag</AlertDescription>
<a href="/test" data-testid={`${variant}-link`}>
Link
</a>
</Alert>
);
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();
});
});
});

View File

@@ -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:

View File

@@ -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(
<CodeBlock language={language} noMargin>
{codeSnippet}
</CodeBlock>
);
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(
<CodeBlock language={language} noMargin={false}>
{codeSnippet}
</CodeBlock>
);
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(<CodeBlock language={language}>{codeSnippet}</CodeBlock>);
const containerElement = screen.getByText(codeSnippet).closest("div");
expect(containerElement).toHaveClass("mt-4");
});
});

View File

@@ -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 (
<div className="group relative mt-4 rounded-md text-sm text-slate-200">
<div className={cn("group relative rounded-md text-sm text-slate-200", noMargin ? "" : "mt-4")}>
{showCopyToClipboard && (
<div className="absolute right-2 top-2 z-20 flex cursor-pointer items-center justify-center p-1.5 text-slate-500 hover:text-slate-900">
<CopyIcon

View File

@@ -38,8 +38,8 @@ describe("Typography Components", () => {
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>Large text</Large>);
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");
});

View File

@@ -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<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>((props, ref) => {
@@ -40,7 +41,7 @@ const H3 = forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElemen
<h3
{...props}
ref={ref}
className={cn("scroll-m-20 text-2xl font-semibold tracking-tight text-slate-800", props.className)}>
className={cn("scroll-m-20 text-lg tracking-tight text-slate-800", props.className)}>
{props.children}
</h3>
);
@@ -54,7 +55,7 @@ const H4 = forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElemen
<h4
{...props}
ref={ref}
className={cn("scroll-m-20 text-xl font-semibold tracking-tight text-slate-800", props.className)}>
className={cn("scroll-m-20 text-base tracking-tight text-slate-800", props.className)}>
{props.children}
</h4>
);
@@ -87,18 +88,53 @@ export { P };
const Large = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((props, ref) => {
return (
<div {...props} ref={ref} className={cn("text-lg font-semibold", props.className)}>
<p {...props} ref={ref} className={cn("text-lg", props.className)}>
{props.children}
</div>
</p>
);
});
Large.displayName = "Large";
export { Large };
const Small = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>((props, ref) => {
const Base = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((props, ref) => {
return (
<p {...props} ref={ref} className={cn("text-sm font-medium leading-none", props.className)}>
<p {...props} ref={ref} className={cn("text-base", props.className)}>
{props.children}
</p>
);
});
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<HTMLParagraphElement> {
color?: "default" | "muted";
margin?: "default" | "headerDescription";
}
const Small = forwardRef<HTMLParagraphElement, SmallProps>((props, ref) => {
return (
<p
{...props}
ref={ref}
className={cn(
smallVariants({ color: props.color ?? "default", margin: props.margin ?? "default" }),
props.className
)}>
{props.children}
</p>
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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**