mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-07 08:50:25 -06:00
feat: new share modal website embed and pop out (#6217)
This commit is contained in:
committed by
GitHub
parent
17d60eb1e7
commit
8af6c15998
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "分享結果",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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 |
@@ -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**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user