mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-11 17:19:33 -06:00
feat: New share modal - "In App" tab (#6225)
Co-authored-by: Jakob Schott <154420406+jakobsitory@users.noreply.github.com> Co-authored-by: Jakob Schott <jakob@formbricks.com>
This commit is contained in:
committed by
GitHub
parent
b8b5eead7a
commit
53213b41ee
@@ -0,0 +1,157 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { EnvironmentContextWrapper, useEnvironment } from "./environment-context";
|
||||
|
||||
// Mock environment data
|
||||
const mockEnvironment: TEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
projectId: "test-project-id",
|
||||
appSetupCompleted: true,
|
||||
};
|
||||
|
||||
// Mock project data
|
||||
const mockProject = {
|
||||
id: "test-project-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
organizationId: "test-org-id",
|
||||
config: {
|
||||
channel: "app",
|
||||
industry: "saas",
|
||||
},
|
||||
linkSurveyBranding: true,
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
brandColor: {
|
||||
light: "#ffffff",
|
||||
dark: "#000000",
|
||||
},
|
||||
questionColor: {
|
||||
light: "#000000",
|
||||
dark: "#ffffff",
|
||||
},
|
||||
inputColor: {
|
||||
light: "#000000",
|
||||
dark: "#ffffff",
|
||||
},
|
||||
inputBorderColor: {
|
||||
light: "#cccccc",
|
||||
dark: "#444444",
|
||||
},
|
||||
cardBackgroundColor: {
|
||||
light: "#ffffff",
|
||||
dark: "#000000",
|
||||
},
|
||||
cardBorderColor: {
|
||||
light: "#cccccc",
|
||||
dark: "#444444",
|
||||
},
|
||||
isDarkModeEnabled: false,
|
||||
isLogoHidden: false,
|
||||
hideProgressBar: false,
|
||||
roundness: 8,
|
||||
cardArrangement: {
|
||||
linkSurveys: "casual",
|
||||
appSurveys: "casual",
|
||||
},
|
||||
},
|
||||
recontactDays: 30,
|
||||
inAppSurveyBranding: true,
|
||||
logo: {
|
||||
url: "test-logo.png",
|
||||
bgColor: "#ffffff",
|
||||
},
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
} as TProject;
|
||||
|
||||
// Test component that uses the hook
|
||||
const TestComponent = () => {
|
||||
const { environment, project } = useEnvironment();
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="environment-id">{environment.id}</div>
|
||||
<div data-testid="environment-type">{environment.type}</div>
|
||||
<div data-testid="project-id">{project.id}</div>
|
||||
<div data-testid="project-organization-id">{project.organizationId}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe("EnvironmentContext", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("provides environment and project data to child components", () => {
|
||||
render(
|
||||
<EnvironmentContextWrapper environment={mockEnvironment} project={mockProject}>
|
||||
<TestComponent />
|
||||
</EnvironmentContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("environment-id")).toHaveTextContent("test-env-id");
|
||||
expect(screen.getByTestId("environment-type")).toHaveTextContent("development");
|
||||
expect(screen.getByTestId("project-id")).toHaveTextContent("test-project-id");
|
||||
expect(screen.getByTestId("project-organization-id")).toHaveTextContent("test-org-id");
|
||||
});
|
||||
|
||||
test("throws error when useEnvironment is used outside of provider", () => {
|
||||
const TestComponentWithoutProvider = () => {
|
||||
useEnvironment();
|
||||
return <div>Should not render</div>;
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<TestComponentWithoutProvider />);
|
||||
}).toThrow("useEnvironment must be used within an EnvironmentProvider");
|
||||
});
|
||||
|
||||
test("updates context value when environment or project changes", () => {
|
||||
const { rerender } = render(
|
||||
<EnvironmentContextWrapper environment={mockEnvironment} project={mockProject}>
|
||||
<TestComponent />
|
||||
</EnvironmentContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("environment-type")).toHaveTextContent("development");
|
||||
|
||||
const updatedEnvironment = {
|
||||
...mockEnvironment,
|
||||
type: "production" as const,
|
||||
};
|
||||
|
||||
rerender(
|
||||
<EnvironmentContextWrapper environment={updatedEnvironment} project={mockProject}>
|
||||
<TestComponent />
|
||||
</EnvironmentContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("environment-type")).toHaveTextContent("production");
|
||||
});
|
||||
|
||||
test("memoizes context value correctly", () => {
|
||||
const { rerender } = render(
|
||||
<EnvironmentContextWrapper environment={mockEnvironment} project={mockProject}>
|
||||
<TestComponent />
|
||||
</EnvironmentContextWrapper>
|
||||
);
|
||||
|
||||
// Re-render with same props
|
||||
rerender(
|
||||
<EnvironmentContextWrapper environment={mockEnvironment} project={mockProject}>
|
||||
<TestComponent />
|
||||
</EnvironmentContextWrapper>
|
||||
);
|
||||
|
||||
// Should still work correctly
|
||||
expect(screen.getByTestId("environment-id")).toHaveTextContent("test-env-id");
|
||||
expect(screen.getByTestId("project-id")).toHaveTextContent("test-project-id");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
|
||||
export interface EnvironmentContextType {
|
||||
environment: TEnvironment;
|
||||
project: TProject;
|
||||
}
|
||||
|
||||
const EnvironmentContext = createContext<EnvironmentContextType | null>(null);
|
||||
|
||||
export const useEnvironment = () => {
|
||||
const context = useContext(EnvironmentContext);
|
||||
if (!context) {
|
||||
throw new Error("useEnvironment must be used within an EnvironmentProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Client wrapper component to be used in server components
|
||||
interface EnvironmentContextWrapperProps {
|
||||
environment: TEnvironment;
|
||||
project: TProject;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const EnvironmentContextWrapper = ({
|
||||
environment,
|
||||
project,
|
||||
children,
|
||||
}: EnvironmentContextWrapperProps) => {
|
||||
const environmentContextValue = useMemo(
|
||||
() => ({
|
||||
environment,
|
||||
project,
|
||||
}),
|
||||
[environment, project]
|
||||
);
|
||||
|
||||
return (
|
||||
<EnvironmentContext.Provider value={environmentContextValue}>{children}</EnvironmentContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||
@@ -5,6 +6,7 @@ import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { Session } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TMembership } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
@@ -13,12 +15,20 @@ import EnvLayout from "./layout";
|
||||
|
||||
// Mock sub-components to render identifiable elements
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/EnvironmentLayout", () => ({
|
||||
EnvironmentLayout: ({ children }: any) => <div data-testid="EnvironmentLayout">{children}</div>,
|
||||
EnvironmentLayout: ({ children, environmentId, session }: any) => (
|
||||
<div data-testid="EnvironmentLayout" data-environment-id={environmentId} data-session={session?.user?.id}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({
|
||||
EnvironmentIdBaseLayout: ({ children, environmentId }: any) => (
|
||||
<div data-testid="EnvironmentIdBaseLayout">
|
||||
{environmentId}
|
||||
EnvironmentIdBaseLayout: ({ children, environmentId, session, user, organization }: any) => (
|
||||
<div
|
||||
data-testid="EnvironmentIdBaseLayout"
|
||||
data-environment-id={environmentId}
|
||||
data-session={session?.user?.id}
|
||||
data-user={user?.id}
|
||||
data-organization={organization?.id}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
@@ -27,7 +37,24 @@ vi.mock("@/modules/ui/components/toaster-client", () => ({
|
||||
ToasterClient: () => <div data-testid="ToasterClient" />,
|
||||
}));
|
||||
vi.mock("./components/EnvironmentStorageHandler", () => ({
|
||||
default: ({ environmentId }: any) => <div data-testid="EnvironmentStorageHandler">{environmentId}</div>,
|
||||
default: ({ environmentId }: any) => (
|
||||
<div data-testid="EnvironmentStorageHandler" data-environment-id={environmentId} />
|
||||
),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/context/environment-context", () => ({
|
||||
EnvironmentContextWrapper: ({ children, environment, project }: any) => (
|
||||
<div
|
||||
data-testid="EnvironmentContextWrapper"
|
||||
data-environment-id={environment?.id}
|
||||
data-project-id={project?.id}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock navigation
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mocks for dependencies
|
||||
@@ -37,26 +64,43 @@ vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
vi.mock("@/lib/project/service", () => ({
|
||||
getProjectByEnvironmentId: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/environment/service", () => ({
|
||||
getEnvironment: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/membership/service", () => ({
|
||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("EnvLayout", () => {
|
||||
const mockSession = { user: { id: "user1" } } as Session;
|
||||
const mockUser = { id: "user1", email: "user1@example.com" } as TUser;
|
||||
const mockOrganization = { id: "org1", name: "Org1", billing: {} } as TOrganization;
|
||||
const mockProject = { id: "proj1", name: "Test Project" } as TProject;
|
||||
const mockEnvironment = { id: "env1", type: "production" } as TEnvironment;
|
||||
const mockMembership = {
|
||||
id: "member1",
|
||||
role: "owner",
|
||||
organizationId: "org1",
|
||||
userId: "user1",
|
||||
accepted: true,
|
||||
} as TMembership;
|
||||
const mockTranslation = ((key: string) => key) as any;
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders successfully when all dependencies return valid data", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
|
||||
session: { user: { id: "user1" } } as Session,
|
||||
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||
t: mockTranslation,
|
||||
session: mockSession,
|
||||
user: mockUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ id: "proj1" } as TProject);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce({
|
||||
id: "member1",
|
||||
} as unknown as TMembership);
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
|
||||
vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership);
|
||||
|
||||
const result = await EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
@@ -64,56 +108,43 @@ describe("EnvLayout", () => {
|
||||
});
|
||||
render(result);
|
||||
|
||||
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveTextContent("env1");
|
||||
expect(screen.getByTestId("EnvironmentStorageHandler")).toHaveTextContent("env1");
|
||||
expect(screen.getByTestId("EnvironmentLayout")).toBeDefined();
|
||||
// Verify main layout structure
|
||||
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-environment-id", "env1");
|
||||
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-session", "user1");
|
||||
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-user", "user1");
|
||||
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveAttribute("data-organization", "org1");
|
||||
|
||||
// Verify environment storage handler
|
||||
expect(screen.getByTestId("EnvironmentStorageHandler")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("EnvironmentStorageHandler")).toHaveAttribute("data-environment-id", "env1");
|
||||
|
||||
// Verify context wrapper
|
||||
expect(screen.getByTestId("EnvironmentContextWrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-environment-id", "env1");
|
||||
expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-project-id", "proj1");
|
||||
|
||||
// Verify environment layout
|
||||
expect(screen.getByTestId("EnvironmentLayout")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("EnvironmentLayout")).toHaveAttribute("data-environment-id", "env1");
|
||||
expect(screen.getByTestId("EnvironmentLayout")).toHaveAttribute("data-session", "user1");
|
||||
|
||||
// Verify children are rendered
|
||||
expect(screen.getByTestId("child")).toHaveTextContent("Content");
|
||||
|
||||
// Verify all services were called with correct parameters
|
||||
expect(environmentIdLayoutChecks).toHaveBeenCalledWith("env1");
|
||||
expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1");
|
||||
expect(getEnvironment).toHaveBeenCalledWith("env1");
|
||||
expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1");
|
||||
});
|
||||
|
||||
test("throws error if project is not found", async () => {
|
||||
test("redirects when session is null", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: { user: { id: "user1" } } as Session,
|
||||
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||
});
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce({
|
||||
id: "member1",
|
||||
} as unknown as TMembership);
|
||||
|
||||
await expect(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
children: <div>Content</div>,
|
||||
})
|
||||
).rejects.toThrow("common.project_not_found");
|
||||
});
|
||||
|
||||
test("throws error if membership is not found", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: { user: { id: "user1" } } as Session,
|
||||
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||
});
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ id: "proj1" } as TProject);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
children: <div>Content</div>,
|
||||
})
|
||||
).rejects.toThrow("common.membership_not_found");
|
||||
});
|
||||
|
||||
test("calls redirect when session is null", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: undefined as unknown as Session,
|
||||
user: undefined as unknown as TUser,
|
||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||
t: mockTranslation,
|
||||
session: null as unknown as Session,
|
||||
user: mockUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
vi.mocked(redirect).mockImplementationOnce(() => {
|
||||
throw new Error("Redirect called");
|
||||
@@ -125,18 +156,16 @@ describe("EnvLayout", () => {
|
||||
children: <div>Content</div>,
|
||||
})
|
||||
).rejects.toThrow("Redirect called");
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
||||
});
|
||||
|
||||
test("throws error if user is null", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: ((key: string) => key) as any,
|
||||
session: { user: { id: "user1" } } as Session,
|
||||
user: undefined as unknown as TUser,
|
||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||
});
|
||||
|
||||
vi.mocked(redirect).mockImplementationOnce(() => {
|
||||
throw new Error("Redirect called");
|
||||
t: mockTranslation,
|
||||
session: mockSession,
|
||||
user: null as unknown as TUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -145,5 +174,154 @@ describe("EnvLayout", () => {
|
||||
children: <div>Content</div>,
|
||||
})
|
||||
).rejects.toThrow("common.user_not_found");
|
||||
|
||||
// Verify redirect was not called
|
||||
expect(redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("throws error if project is not found", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: mockTranslation,
|
||||
session: mockSession,
|
||||
user: mockUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null);
|
||||
vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
|
||||
|
||||
await expect(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
children: <div>Content</div>,
|
||||
})
|
||||
).rejects.toThrow("common.project_not_found");
|
||||
|
||||
// Verify both project and environment were called in Promise.all
|
||||
expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1");
|
||||
expect(getEnvironment).toHaveBeenCalledWith("env1");
|
||||
});
|
||||
|
||||
test("throws error if environment is not found", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: mockTranslation,
|
||||
session: mockSession,
|
||||
user: mockUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
|
||||
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
children: <div>Content</div>,
|
||||
})
|
||||
).rejects.toThrow("common.environment_not_found");
|
||||
|
||||
// Verify both project and environment were called in Promise.all
|
||||
expect(getProjectByEnvironmentId).toHaveBeenCalledWith("env1");
|
||||
expect(getEnvironment).toHaveBeenCalledWith("env1");
|
||||
});
|
||||
|
||||
test("throws error if membership is not found", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: mockTranslation,
|
||||
session: mockSession,
|
||||
user: mockUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
|
||||
vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
children: <div>Content</div>,
|
||||
})
|
||||
).rejects.toThrow("common.membership_not_found");
|
||||
|
||||
expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1");
|
||||
});
|
||||
|
||||
test("handles Promise.all correctly for project and environment", async () => {
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: mockTranslation,
|
||||
session: mockSession,
|
||||
user: mockUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
|
||||
// Mock Promise.all to verify it's called correctly
|
||||
const getProjectSpy = vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
|
||||
const getEnvironmentSpy = vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership);
|
||||
|
||||
const result = await EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
children: <div data-testid="child">Content</div>,
|
||||
});
|
||||
render(result);
|
||||
|
||||
// Verify both calls were made
|
||||
expect(getProjectSpy).toHaveBeenCalledWith("env1");
|
||||
expect(getEnvironmentSpy).toHaveBeenCalledWith("env1");
|
||||
|
||||
// Verify successful rendering
|
||||
expect(screen.getByTestId("child")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles different environment types correctly", async () => {
|
||||
const developmentEnvironment = { id: "env1", type: "development" } as TEnvironment;
|
||||
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: mockTranslation,
|
||||
session: mockSession,
|
||||
user: mockUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
|
||||
vi.mocked(getEnvironment).mockResolvedValueOnce(developmentEnvironment);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(mockMembership);
|
||||
|
||||
const result = await EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
children: <div data-testid="child">Content</div>,
|
||||
});
|
||||
render(result);
|
||||
|
||||
// Verify context wrapper receives the development environment
|
||||
expect(screen.getByTestId("EnvironmentContextWrapper")).toHaveAttribute("data-environment-id", "env1");
|
||||
expect(screen.getByTestId("child")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles different user roles correctly", async () => {
|
||||
const memberMembership = {
|
||||
id: "member1",
|
||||
role: "member",
|
||||
organizationId: "org1",
|
||||
userId: "user1",
|
||||
accepted: true,
|
||||
} as TMembership;
|
||||
|
||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||
t: mockTranslation,
|
||||
session: mockSession,
|
||||
user: mockUser,
|
||||
organization: mockOrganization,
|
||||
});
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(mockProject);
|
||||
vi.mocked(getEnvironment).mockResolvedValueOnce(mockEnvironment);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(memberMembership);
|
||||
|
||||
const result = await EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env1" }),
|
||||
children: <div data-testid="child">Content</div>,
|
||||
});
|
||||
render(result);
|
||||
|
||||
// Verify successful rendering with member role
|
||||
expect(screen.getByTestId("child")).toBeInTheDocument();
|
||||
expect(getMembershipByUserIdOrganizationId).toHaveBeenCalledWith("user1", "org1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
||||
import { EnvironmentContextWrapper } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||
@@ -11,7 +13,6 @@ const EnvLayout = async (props: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const { t, session, user, organization } = await environmentIdLayoutChecks(params.environmentId);
|
||||
@@ -24,11 +25,19 @@ const EnvLayout = async (props: {
|
||||
throw new Error(t("common.user_not_found"));
|
||||
}
|
||||
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
const [project, environment] = await Promise.all([
|
||||
getProjectByEnvironmentId(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
|
||||
|
||||
if (!membership) {
|
||||
@@ -42,9 +51,11 @@ const EnvLayout = async (props: {
|
||||
user={user}
|
||||
organization={organization}>
|
||||
<EnvironmentStorageHandler environmentId={params.environmentId} />
|
||||
<EnvironmentLayout environmentId={params.environmentId} session={session}>
|
||||
{children}
|
||||
</EnvironmentLayout>
|
||||
<EnvironmentContextWrapper environment={environment} project={project}>
|
||||
<EnvironmentLayout environmentId={params.environmentId} session={session}>
|
||||
{children}
|
||||
</EnvironmentLayout>
|
||||
</EnvironmentContextWrapper>
|
||||
</EnvironmentIdBaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,450 +1,437 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import toast from "react-hot-toast";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { SurveyAnalysisCTA } from "./SurveyAnalysisCTA";
|
||||
|
||||
vi.mock("@/lib/utils/action-client-middleware", () => ({
|
||||
checkAuthorizationUpdated: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ee/audit-logs/lib/utils", () => ({
|
||||
withAuditLogging: vi.fn((...args: any[]) => {
|
||||
// Check if the last argument is a function and return it directly
|
||||
if (typeof args[args.length - 1] === "function") {
|
||||
return args[args.length - 1];
|
||||
}
|
||||
// Otherwise, return a new function that takes a function as an argument and returns it
|
||||
return (fn: any) => fn;
|
||||
// Mock the useTranslate hook
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => {
|
||||
if (key === "environments.surveys.summary.configure_alerts") {
|
||||
return "Configure alerts";
|
||||
}
|
||||
if (key === "common.preview") {
|
||||
return "Preview";
|
||||
}
|
||||
if (key === "common.edit") {
|
||||
return "Edit";
|
||||
}
|
||||
if (key === "environments.surveys.summary.share_survey") {
|
||||
return "Share survey";
|
||||
}
|
||||
if (key === "environments.surveys.summary.results_are_public") {
|
||||
return "Results are public";
|
||||
}
|
||||
if (key === "environments.surveys.survey_duplicated_successfully") {
|
||||
return "Survey duplicated successfully";
|
||||
}
|
||||
if (key === "environments.surveys.edit.caution_edit_duplicate") {
|
||||
return "Duplicate & Edit";
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockPublicDomain = "https://public-domain.com";
|
||||
// Mock Next.js hooks
|
||||
const mockPush = vi.fn();
|
||||
const mockPathname = "/environments/env-id/surveys/survey-id/summary";
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
|
||||
// Mock constants
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
ENCRYPTION_KEY: "test",
|
||||
ENTERPRISE_LICENSE_KEY: "test",
|
||||
GITHUB_ID: "test",
|
||||
GITHUB_SECRET: "test",
|
||||
GOOGLE_CLIENT_ID: "test",
|
||||
GOOGLE_CLIENT_SECRET: "test",
|
||||
AZUREAD_CLIENT_ID: "mock-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
|
||||
AZUREAD_TENANT_ID: "mock-azuread-tenant-id",
|
||||
OIDC_CLIENT_ID: "mock-oidc-client-id",
|
||||
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
|
||||
OIDC_ISSUER: "mock-oidc-issuer",
|
||||
OIDC_DISPLAY_NAME: "mock-oidc-display-name",
|
||||
OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "mock-webapp-url",
|
||||
IS_PRODUCTION: true,
|
||||
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
||||
SMTP_HOST: "mock-smtp-host",
|
||||
SMTP_PORT: "mock-smtp-port",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
AUDIT_LOG_ENABLED: true,
|
||||
SESSION_MAX_AGE: 1000,
|
||||
REDIS_URL: "mock-url",
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
usePathname: () => mockPathname,
|
||||
useSearchParams: () => mockSearchParams,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/env", () => ({
|
||||
env: {
|
||||
PUBLIC_URL: "https://public-domain.com",
|
||||
// Mock react-hot-toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Create a spy for refreshSingleUseId so we can override it in tests
|
||||
const refreshSingleUseIdSpy = vi.fn(() => Promise.resolve("newSingleUseId"));
|
||||
|
||||
// Mock useSingleUseId hook
|
||||
vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({
|
||||
useSingleUseId: () => ({
|
||||
refreshSingleUseId: refreshSingleUseIdSpy,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
const mockPush = vi.fn();
|
||||
const mockReplace = vi.fn();
|
||||
|
||||
// Mock next/navigation
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: mockPush, replace: mockReplace }),
|
||||
useSearchParams: () => mockSearchParams,
|
||||
usePathname: () => "/current-path",
|
||||
}));
|
||||
|
||||
// Mock copySurveyLink to return a predictable string
|
||||
vi.mock("@/modules/survey/lib/client-utils", () => ({
|
||||
copySurveyLink: vi.fn((url: string, suId: string) => `${url}?suId=${suId}`),
|
||||
}));
|
||||
|
||||
// Mock the copy survey action
|
||||
const mockCopySurveyToOtherEnvironmentAction = vi.fn();
|
||||
vi.mock("@/modules/survey/list/actions", () => ({
|
||||
copySurveyToOtherEnvironmentAction: (args: any) => mockCopySurveyToOtherEnvironmentAction(args),
|
||||
}));
|
||||
|
||||
// Mock getFormattedErrorMessage function
|
||||
// Mock helper functions
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getFormattedErrorMessage: vi.fn((response) => response?.error || "Unknown error"),
|
||||
getFormattedErrorMessage: vi.fn(() => "Error message"),
|
||||
}));
|
||||
|
||||
// Mock ResponseCountProvider dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
||||
useResponseFilter: vi.fn(() => ({ selectedFilter: "all", dateRange: {} })),
|
||||
// Mock actions
|
||||
vi.mock("@/modules/survey/list/actions", () => ({
|
||||
copySurveyToOtherEnvironmentAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions", () => ({
|
||||
getResponseCountAction: vi.fn(() => Promise.resolve({ data: 5 })),
|
||||
// Mock child components
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage",
|
||||
() => ({
|
||||
SuccessMessage: ({ environment, survey }: any) => (
|
||||
<div data-testid="success-message">
|
||||
Success Message for {environment.id} - {survey.id}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal",
|
||||
() => ({
|
||||
ShareSurveyModal: ({ survey, open, setOpen, modalView, user }: any) => (
|
||||
<div data-testid="share-survey-modal" data-open={open} data-modal-view={modalView}>
|
||||
Share Survey Modal for {survey.id} - User: {user.id}
|
||||
<button type="button" onClick={() => setOpen(false)}>
|
||||
Close Modal
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown",
|
||||
() => ({
|
||||
SurveyStatusDropdown: ({ environment, survey }: any) => (
|
||||
<div data-testid="survey-status-dropdown">
|
||||
Status Dropdown for {environment.id} - {survey.id}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/modules/survey/components/edit-public-survey-alert-dialog", () => ({
|
||||
EditPublicSurveyAlertDialog: ({
|
||||
open,
|
||||
setOpen,
|
||||
isLoading,
|
||||
primaryButtonAction,
|
||||
primaryButtonText,
|
||||
secondaryButtonAction,
|
||||
secondaryButtonText,
|
||||
}: any) => (
|
||||
<div data-testid="edit-public-survey-alert-dialog" data-open={open} data-loading={isLoading}>
|
||||
<button type="button" onClick={primaryButtonAction} data-testid="primary-button">
|
||||
{primaryButtonText}
|
||||
</button>
|
||||
<button type="button" onClick={secondaryButtonAction} data-testid="secondary-button">
|
||||
{secondaryButtonText}
|
||||
</button>
|
||||
<button type="button" onClick={() => setOpen(false)}>
|
||||
Close Dialog
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/lib/surveys/surveys", () => ({
|
||||
getFormattedFilters: vi.fn(() => []),
|
||||
// Mock UI components
|
||||
vi.mock("@/modules/ui/components/badge", () => ({
|
||||
Badge: ({ type, size, className, text }: any) => (
|
||||
<div data-testid="badge" data-type={type} data-size={size} className={className}>
|
||||
{text}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/share/[sharingKey]/actions", () => ({
|
||||
getResponseCountBySurveySharingKeyAction: vi.fn(() => Promise.resolve({ data: 5 })),
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick, className }: any) => (
|
||||
<button type="button" data-testid="button" onClick={onClick} className={className}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/getPublicUrl", () => ({
|
||||
getPublicDomain: vi.fn(() => mockPublicDomain),
|
||||
vi.mock("@/modules/ui/components/iconbar", () => ({
|
||||
IconBar: ({ actions }: any) => (
|
||||
<div data-testid="icon-bar">
|
||||
{actions
|
||||
.filter((action: any) => action.isVisible)
|
||||
.map((action: any, index: number) => (
|
||||
<button
|
||||
type="button"
|
||||
key={index} // NOSONAR // We don't need to check this in the test
|
||||
onClick={action.onClick}
|
||||
title={action.tooltip}
|
||||
data-testid={`icon-bar-action-${index}`}>
|
||||
<action.icon />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.spyOn(toast, "success");
|
||||
vi.spyOn(toast, "error");
|
||||
// Mock lucide-react icons
|
||||
vi.mock("lucide-react", () => ({
|
||||
BellRing: () => <svg data-testid="bell-ring-icon" />,
|
||||
Eye: () => <svg data-testid="eye-icon" />,
|
||||
SquarePenIcon: () => <svg data-testid="square-pen-icon" />,
|
||||
}));
|
||||
|
||||
// Mock clipboard API
|
||||
const writeTextMock = vi.fn().mockImplementation(() => Promise.resolve());
|
||||
// Mock data
|
||||
const mockEnvironment: TEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
projectId: "test-project-id",
|
||||
appSetupCompleted: true,
|
||||
};
|
||||
|
||||
// Define it at the global level
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText: writeTextMock },
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const dummySurvey = {
|
||||
id: "survey123",
|
||||
type: "link",
|
||||
environmentId: "env123",
|
||||
status: "inProgress",
|
||||
resultShareKey: null,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const dummyAppSurvey = {
|
||||
id: "survey123",
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "test-survey-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
environmentId: "env123",
|
||||
environmentId: "test-env-id",
|
||||
status: "inProgress",
|
||||
} as unknown as TSurvey;
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
|
||||
const dummyEnvironment = { id: "env123", appSetupCompleted: true } as TEnvironment;
|
||||
const dummyUser = { id: "user123", name: "Test User" } as TUser;
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
welcomeCard: { enabled: false, timeToFinish: false, showResponseCount: false },
|
||||
questions: [],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false },
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
|
||||
segment: null,
|
||||
languages: [],
|
||||
showLanguageSwitch: false,
|
||||
singleUse: { enabled: false, isEncrypted: false },
|
||||
projectOverwrites: null,
|
||||
surveyClosedMessage: null,
|
||||
delay: 0,
|
||||
isVerifyEmailEnabled: false,
|
||||
createdBy: null,
|
||||
variables: [],
|
||||
followUps: [],
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
styling: null,
|
||||
pin: null,
|
||||
recaptcha: null,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
isBackButtonHidden: false,
|
||||
resultShareKey: null,
|
||||
};
|
||||
|
||||
const mockUser: TUser = {
|
||||
id: "test-user-id",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "https://example.com/avatar.jpg",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
|
||||
role: "other",
|
||||
objective: "other",
|
||||
locale: "en-US",
|
||||
lastLoginAt: new Date(),
|
||||
isActive: true,
|
||||
notificationSettings: {
|
||||
alert: {
|
||||
weeklySummary: true,
|
||||
responseFinished: true,
|
||||
},
|
||||
weeklySummary: {
|
||||
test: true,
|
||||
},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
const mockSegments: TSegment[] = [];
|
||||
|
||||
const defaultProps = {
|
||||
survey: mockSurvey,
|
||||
environment: mockEnvironment,
|
||||
isReadOnly: false,
|
||||
user: mockUser,
|
||||
publicDomain: "https://example.com",
|
||||
responseCount: 0,
|
||||
segments: mockSegments,
|
||||
isContactsEnabled: true,
|
||||
isFormbricksCloud: false,
|
||||
};
|
||||
|
||||
describe("SurveyAnalysisCTA", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockSearchParams.delete("share"); // reset params
|
||||
vi.clearAllMocks();
|
||||
mockSearchParams.delete("share");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("Edit functionality", () => {
|
||||
test("opens EditPublicSurveyAlertDialog when edit icon is clicked and response count > 0", async () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
test("renders share survey button", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
// Find the edit button
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButton);
|
||||
|
||||
// Check if dialog is shown
|
||||
const dialogTitle = screen.getByText("environments.surveys.edit.caution_edit_published_survey");
|
||||
expect(dialogTitle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("navigates directly to edit page when response count = 0", async () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={0}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the edit button
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButton);
|
||||
|
||||
// Should navigate directly to edit page
|
||||
expect(mockPush).toHaveBeenCalledWith(
|
||||
`/environments/${dummyEnvironment.id}/surveys/${dummySurvey.id}/edit`
|
||||
);
|
||||
});
|
||||
|
||||
test("doesn't show edit button when isReadOnly is true", () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={true}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const editButton = screen.queryByRole("button", { name: "common.edit" });
|
||||
expect(editButton).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Share survey")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Duplicate functionality", () => {
|
||||
test("duplicates survey and redirects on primary button click", async () => {
|
||||
mockCopySurveyToOtherEnvironmentAction.mockResolvedValue({
|
||||
data: { id: "newSurvey456" },
|
||||
});
|
||||
test("renders success message component", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
fireEvent.click(editButton);
|
||||
|
||||
const primaryButton = await screen.findByText("environments.surveys.edit.caution_edit_duplicate");
|
||||
fireEvent.click(primaryButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCopySurveyToOtherEnvironmentAction).toHaveBeenCalledWith({
|
||||
environmentId: "env123",
|
||||
surveyId: "survey123",
|
||||
targetEnvironmentId: "env123",
|
||||
});
|
||||
expect(mockPush).toHaveBeenCalledWith("/environments/env123/surveys/newSurvey456/edit");
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.survey_duplicated_successfully");
|
||||
});
|
||||
});
|
||||
|
||||
test("shows error toast on duplication failure", async () => {
|
||||
const error = { error: "Duplication failed" };
|
||||
mockCopySurveyToOtherEnvironmentAction.mockResolvedValue(error);
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
fireEvent.click(editButton);
|
||||
|
||||
const primaryButton = await screen.findByText("environments.surveys.edit.caution_edit_duplicate");
|
||||
fireEvent.click(primaryButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith("Duplication failed");
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId("success-message")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Share button and modal", () => {
|
||||
test("opens share modal when 'Share survey' button is clicked", async () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
test("renders survey status dropdown when app setup is completed", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
const shareButton = screen.getByText("environments.surveys.summary.share_survey");
|
||||
fireEvent.click(shareButton);
|
||||
|
||||
// The share button opens the embed modal, not a URL
|
||||
// We can verify this by checking that the ShareEmbedSurvey component is rendered
|
||||
// with the embed modal open
|
||||
expect(screen.getByText("environments.surveys.summary.share_survey")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ShareEmbedSurvey component when share modal is open", async () => {
|
||||
mockSearchParams.set("share", "true");
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Assuming ShareEmbedSurvey renders a dialog with a specific title when open
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
expect(dialog).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("General UI and visibility", () => {
|
||||
test("shows public results badge when resultShareKey is present", () => {
|
||||
const surveyWithShareKey = { ...dummySurvey, resultShareKey: "someKey" } as TSurvey;
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={surveyWithShareKey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
test("does not render survey status dropdown when read-only", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} isReadOnly={true} />);
|
||||
|
||||
expect(screen.getByText("environments.surveys.summary.results_are_public")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId("survey-status-dropdown")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows SurveyStatusDropdown for non-draft surveys", () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
test("renders icon bar with correct actions", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId("icon-bar")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("icon-bar-action-0")).toBeInTheDocument(); // Bell ring
|
||||
expect(screen.getByTestId("icon-bar-action-1")).toBeInTheDocument(); // Square pen
|
||||
});
|
||||
|
||||
test("does not show SurveyStatusDropdown for draft surveys", () => {
|
||||
const draftSurvey = { ...dummySurvey, status: "draft" } as TSurvey;
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={draftSurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||
});
|
||||
test("shows preview icon for link surveys", () => {
|
||||
const linkSurvey = { ...mockSurvey, type: "link" as const };
|
||||
render(<SurveyAnalysisCTA {...defaultProps} survey={linkSurvey} />);
|
||||
|
||||
test("hides status dropdown and edit actions when isReadOnly is true", () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={true}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("icon-bar-action-1")).toHaveAttribute("title", "Preview");
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "common.edit" })).not.toBeInTheDocument();
|
||||
});
|
||||
test("shows public results badge when resultShareKey exists", () => {
|
||||
const surveyWithShareKey = { ...mockSurvey, resultShareKey: "share-key" };
|
||||
render(<SurveyAnalysisCTA {...defaultProps} survey={surveyWithShareKey} />);
|
||||
|
||||
test("shows preview button for link surveys", () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "common.preview" })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId("badge")).toBeInTheDocument();
|
||||
expect(screen.getByText("Results are public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides preview button for app surveys", () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummyAppSurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
publicDomain={mockPublicDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
segments={[]}
|
||||
isContactsEnabled={false}
|
||||
isFormbricksCloud={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByRole("button", { name: "common.preview" })).not.toBeInTheDocument();
|
||||
});
|
||||
test("opens share modal when share button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByText("Share survey"));
|
||||
|
||||
expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true");
|
||||
});
|
||||
|
||||
test("opens share modal when share param is true", () => {
|
||||
mockSearchParams.set("share", "true");
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true");
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-modal-view", "start");
|
||||
});
|
||||
|
||||
test("navigates to edit when edit button is clicked and no responses", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByTestId("icon-bar-action-1"));
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith("/environments/test-env-id/surveys/test-survey-id/edit");
|
||||
});
|
||||
|
||||
test("shows caution dialog when edit button is clicked and has responses", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
await user.click(screen.getByTestId("icon-bar-action-1"));
|
||||
|
||||
expect(screen.getByTestId("edit-public-survey-alert-dialog")).toHaveAttribute("data-open", "true");
|
||||
});
|
||||
|
||||
test("navigates to notifications when bell icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByTestId("icon-bar-action-0"));
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith("/environments/test-env-id/settings/notifications");
|
||||
});
|
||||
|
||||
test("opens preview window when preview icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const linkSurvey = { ...mockSurvey, type: "link" as const };
|
||||
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
|
||||
render(<SurveyAnalysisCTA {...defaultProps} survey={linkSurvey} />);
|
||||
|
||||
await user.click(screen.getByTestId("icon-bar-action-1"));
|
||||
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith("https://example.com/s/test-survey-id?preview=true", "_blank");
|
||||
windowOpenSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("does not show icon bar actions when read-only", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} isReadOnly={true} />);
|
||||
|
||||
const iconBar = screen.getByTestId("icon-bar");
|
||||
expect(iconBar).toBeInTheDocument();
|
||||
// Should only show preview icon for link surveys, but this is app survey
|
||||
expect(screen.queryByTestId("icon-bar-action-0")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles modal close correctly", async () => {
|
||||
mockSearchParams.set("share", "true");
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
// Verify modal is open initially
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true");
|
||||
|
||||
await user.click(screen.getByText("Close Modal"));
|
||||
|
||||
// Verify modal is closed
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "false");
|
||||
});
|
||||
|
||||
test("shows status dropdown for link surveys", () => {
|
||||
const linkSurvey = { ...mockSurvey, type: "link" as const };
|
||||
render(<SurveyAnalysisCTA {...defaultProps} survey={linkSurvey} />);
|
||||
|
||||
expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not show status dropdown for draft surveys", () => {
|
||||
const draftSurvey = { ...mockSurvey, status: "draft" as const };
|
||||
render(<SurveyAnalysisCTA {...defaultProps} survey={draftSurvey} />);
|
||||
|
||||
expect(screen.queryByTestId("survey-status-dropdown")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not show status dropdown when app setup is not completed", () => {
|
||||
const environmentWithoutAppSetup = { ...mockEnvironment, appSetupCompleted: false };
|
||||
render(<SurveyAnalysisCTA {...defaultProps} environment={environmentWithoutAppSetup} />);
|
||||
|
||||
expect(screen.queryByTestId("survey-status-dropdown")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with all props", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("icon-bar")).toBeInTheDocument();
|
||||
expect(screen.getByText("Share survey")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("success-message")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,12 +71,16 @@ export const SurveyAnalysisCTA = ({
|
||||
|
||||
const handleShareModalToggle = (open: boolean) => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (open) {
|
||||
const currentShareParam = params.get("share") === "true";
|
||||
|
||||
if (open && !currentShareParam) {
|
||||
params.set("share", "true");
|
||||
} else {
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
} else if (!open && currentShareParam) {
|
||||
params.delete("share");
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
}
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
|
||||
setModalState((prev) => ({ ...prev, start: open }));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ShareViewType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
@@ -8,122 +7,274 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { ShareSurveyModal } from "./share-survey-modal";
|
||||
|
||||
// Mock child components to simplify testing
|
||||
// Mock getPublicDomain - must be first to prevent server-side env access
|
||||
vi.mock("@/lib/getPublicUrl", () => ({
|
||||
getPublicDomain: vi.fn().mockReturnValue("https://example.com"),
|
||||
}));
|
||||
|
||||
// Mock env to prevent server-side env access
|
||||
vi.mock("@/lib/env", () => ({
|
||||
env: {
|
||||
IS_FORMBRICKS_CLOUD: "0",
|
||||
NODE_ENV: "test",
|
||||
E2E_TESTING: "0",
|
||||
ENCRYPTION_KEY: "test-encryption-key-32-characters",
|
||||
WEBAPP_URL: "https://example.com",
|
||||
CRON_SECRET: "test-cron-secret",
|
||||
PUBLIC_URL: "https://example.com",
|
||||
VERCEL_URL: "",
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the useTranslate hook
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"environments.surveys.summary.single_use_links": "Single-use links",
|
||||
"environments.surveys.summary.share_the_link": "Share the link",
|
||||
"environments.surveys.summary.qr_code": "QR Code",
|
||||
"environments.surveys.summary.personal_links": "Personal links",
|
||||
"environments.surveys.summary.embed_in_an_email": "Embed in email",
|
||||
"environments.surveys.summary.embed_on_website": "Embed on website",
|
||||
"environments.surveys.summary.dynamic_popup": "Dynamic popup",
|
||||
"environments.surveys.summary.in_app.title": "In-app survey",
|
||||
"environments.surveys.summary.in_app.description": "Display survey in your app",
|
||||
"environments.surveys.share.anonymous_links.nav_title": "Share the link",
|
||||
"environments.surveys.share.single_use_links.nav_title": "Single-use links",
|
||||
"environments.surveys.share.personal_links.nav_title": "Personal links",
|
||||
"environments.surveys.share.embed_on_website.nav_title": "Embed on website",
|
||||
"environments.surveys.share.send_email.nav_title": "Embed in email",
|
||||
"environments.surveys.share.social_media.title": "Social media",
|
||||
"environments.surveys.share.dynamic_popup.nav_title": "Dynamic popup",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock analysis utils
|
||||
vi.mock("@/modules/analysis/utils", () => ({
|
||||
getSurveyUrl: vi.fn().mockResolvedValue("https://example.com/s/test-survey-id"),
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
log: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock dialog components
|
||||
vi.mock("@/modules/ui/components/dialog", () => ({
|
||||
Dialog: ({ open, onOpenChange, children }: any) => (
|
||||
<div data-testid="dialog" data-open={open} onClick={() => onOpenChange(false)}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogContent: ({ children, width }: any) => (
|
||||
<div data-testid="dialog-content" data-width={width}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogTitle: ({ children }: any) => <div data-testid="dialog-title">{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock VisuallyHidden
|
||||
vi.mock("@radix-ui/react-visually-hidden", () => ({
|
||||
VisuallyHidden: ({ asChild, children }: any) => (
|
||||
<div data-testid="visually-hidden">{asChild ? children : <span>{children}</span>}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab",
|
||||
() => ({
|
||||
AppTab: () => <div data-testid="app-tab">App Tab Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container",
|
||||
() => ({
|
||||
TabContainer: ({ title, description, children }: any) => (
|
||||
<div data-testid="tab-container">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("./shareEmbedModal/share-view", () => ({
|
||||
ShareView: ({ tabs, activeId, setActiveId, survey }: any) => (
|
||||
<div data-testid="share-view">
|
||||
<div data-testid="survey-type">{survey.type}</div>
|
||||
<div data-testid="active-tab">{activeId}</div>
|
||||
{tabs.map((tab: any) => (
|
||||
<button key={tab.id} data-testid={`tab-${tab.id}`} onClick={() => setActiveId(tab.id)}>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
ShareView: ({ tabs, activeId, setActiveId }: any) => (
|
||||
<div data-testid="share-view" data-active-id={activeId}>
|
||||
<h3>Share View</h3>
|
||||
<div data-testid="share-view-data">
|
||||
<div>Active Tab: {activeId}</div>
|
||||
</div>
|
||||
<div data-testid="tabs">
|
||||
{tabs.map((tab: any) => (
|
||||
<button key={tab.id} onClick={() => setActiveId(tab.id)} data-testid={`tab-${tab.id}`}>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./shareEmbedModal/success-view", () => ({
|
||||
SuccessView: ({ survey, handleViewChange, handleEmbedViewWithTab }: any) => (
|
||||
SuccessView: ({
|
||||
survey,
|
||||
surveyUrl,
|
||||
publicDomain,
|
||||
user,
|
||||
tabs,
|
||||
handleViewChange,
|
||||
handleEmbedViewWithTab,
|
||||
}: any) => (
|
||||
<div data-testid="success-view">
|
||||
<div data-testid="survey-id">{survey.id}</div>
|
||||
<button data-testid="change-to-share-view" onClick={() => handleViewChange("share")}>
|
||||
<h3>Success View</h3>
|
||||
<div data-testid="success-view-data">
|
||||
<div>Survey: {survey?.id}</div>
|
||||
<div>URL: {surveyUrl}</div>
|
||||
<div>Domain: {publicDomain}</div>
|
||||
<div>User: {user?.id}</div>
|
||||
</div>
|
||||
<div data-testid="success-tabs">
|
||||
{tabs.map((tab: any) => {
|
||||
// Handle single-use links case
|
||||
let displayLabel = tab.label;
|
||||
if (tab.id === "anon-links" && survey?.singleUse?.enabled) {
|
||||
displayLabel = "Single-use links";
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => handleEmbedViewWithTab(tab.id)}
|
||||
data-testid={`success-tab-${tab.id}`}>
|
||||
{displayLabel}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button onClick={() => handleViewChange("share")} data-testid="go-to-share-view">
|
||||
Go to Share View
|
||||
</button>
|
||||
<button data-testid="embed-with-tab" onClick={() => handleEmbedViewWithTab("email")}>
|
||||
Embed with Email Tab
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock tab components
|
||||
vi.mock("./shareEmbedModal/anonymous-links-tab", () => ({
|
||||
AnonymousLinksTab: () => <div data-testid="anonymous-links-tab">Anonymous Links Tab</div>,
|
||||
}));
|
||||
// Mock lucide-react icons
|
||||
vi.mock("lucide-react", async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
Code2Icon: () => <svg data-testid="code2-icon" />,
|
||||
LinkIcon: () => <svg data-testid="link-icon" />,
|
||||
MailIcon: () => <svg data-testid="mail-icon" />,
|
||||
QrCodeIcon: () => <svg data-testid="qrcode-icon" />,
|
||||
SmartphoneIcon: () => <svg data-testid="smartphone-icon" />,
|
||||
SquareStack: () => <svg data-testid="square-stack-icon" />,
|
||||
UserIcon: () => <svg data-testid="user-icon" />,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./shareEmbedModal/qr-code-tab", () => ({
|
||||
QRCodeTab: () => <div data-testid="qr-code-tab">QR Code Tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./shareEmbedModal/personal-links-tab", () => ({
|
||||
PersonalLinksTab: () => <div data-testid="personal-links-tab">Personal Links Tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./shareEmbedModal/email-tab", () => ({
|
||||
EmailTab: () => <div data-testid="email-tab">Email Tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./shareEmbedModal/website-embed-tab", () => ({
|
||||
WebsiteEmbedTab: () => <div data-testid="website-embed-tab">Website Embed Tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./shareEmbedModal/social-media-tab", () => ({
|
||||
SocialMediaTab: () => <div data-testid="social-media-tab">Social Media Tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./shareEmbedModal/dynamic-popup-tab", () => ({
|
||||
DynamicPopupTab: () => <div data-testid="dynamic-popup-tab">Dynamic Popup Tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./shareEmbedModal/app-tab", () => ({
|
||||
AppTab: () => <div data-testid="app-tab">App Tab</div>,
|
||||
}));
|
||||
|
||||
// Mock analysis utils
|
||||
vi.mock("@/modules/analysis/utils", () => ({
|
||||
getSurveyUrl: vi.fn((survey, publicDomain, type) => `${publicDomain}/${survey.id}?type=${type}`),
|
||||
}));
|
||||
|
||||
const mockUser = {
|
||||
id: "user-123",
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
locale: "en-US",
|
||||
} as TUser;
|
||||
|
||||
const mockSegments: TSegment[] = [
|
||||
{
|
||||
id: "segment-1",
|
||||
title: "Test Segment",
|
||||
description: "Test segment description",
|
||||
environmentId: "env-123",
|
||||
filters: [],
|
||||
isPrivate: false,
|
||||
surveys: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
const mockLinkSurvey = {
|
||||
id: "survey-123",
|
||||
name: "Test Link Survey",
|
||||
// Mock data
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "test-survey-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
environmentId: "env-123",
|
||||
status: "draft",
|
||||
} as TSurvey;
|
||||
environmentId: "test-env-id",
|
||||
status: "inProgress",
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
|
||||
const mockAppSurvey = {
|
||||
id: "app-survey-123",
|
||||
name: "Test App Survey",
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
welcomeCard: { enabled: false, timeToFinish: false, showResponseCount: false },
|
||||
questions: [],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false },
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
|
||||
segment: null,
|
||||
languages: [],
|
||||
showLanguageSwitch: false,
|
||||
singleUse: { enabled: false, isEncrypted: false },
|
||||
projectOverwrites: null,
|
||||
surveyClosedMessage: null,
|
||||
delay: 0,
|
||||
isVerifyEmailEnabled: false,
|
||||
createdBy: null,
|
||||
variables: [],
|
||||
followUps: [],
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
styling: null,
|
||||
pin: null,
|
||||
recaptcha: null,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
isBackButtonHidden: false,
|
||||
resultShareKey: null,
|
||||
};
|
||||
|
||||
const mockAppSurvey: TSurvey = {
|
||||
...mockSurvey,
|
||||
type: "app",
|
||||
environmentId: "env-123",
|
||||
status: "draft",
|
||||
} as TSurvey;
|
||||
};
|
||||
|
||||
const mockUser: TUser = {
|
||||
id: "test-user-id",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "https://example.com/avatar.jpg",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
|
||||
role: "other",
|
||||
objective: "other",
|
||||
locale: "en-US",
|
||||
lastLoginAt: new Date(),
|
||||
isActive: true,
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
const mockSegments: TSegment[] = [];
|
||||
|
||||
const mockSetOpen = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
survey: mockSurvey,
|
||||
publicDomain: "https://example.com",
|
||||
open: true,
|
||||
modalView: "start" as const,
|
||||
setOpen: mockSetOpen,
|
||||
user: mockUser,
|
||||
segments: mockSegments,
|
||||
isContactsEnabled: true,
|
||||
isFormbricksCloud: false,
|
||||
};
|
||||
|
||||
describe("ShareSurveyModal", () => {
|
||||
const defaultProps = {
|
||||
publicDomain: "https://formbricks.com",
|
||||
open: true,
|
||||
modalView: "start" as const,
|
||||
setOpen: vi.fn(),
|
||||
user: mockUser,
|
||||
segments: mockSegments,
|
||||
isContactsEnabled: true,
|
||||
isFormbricksCloud: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -132,183 +283,191 @@ describe("ShareSurveyModal", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("Modal rendering and basic functionality", () => {
|
||||
test("renders modal when open is true", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} />);
|
||||
test("renders dialog when open is true", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("success-view")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId("dialog")).toHaveAttribute("data-open", "true");
|
||||
expect(screen.getByTestId("dialog-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render modal content when open is false", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} open={false} />);
|
||||
test("renders success view when modalView is start", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="start" />);
|
||||
|
||||
expect(screen.queryByTestId("success-view")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("share-view")).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId("success-view")).toBeInTheDocument();
|
||||
expect(screen.getByText("Success View")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls setOpen when modal is closed", async () => {
|
||||
const mockSetOpen = vi.fn();
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} setOpen={mockSetOpen} />);
|
||||
test("renders share view when modalView is share and survey is link type", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="share" />);
|
||||
|
||||
// Simulate modal close by pressing escape
|
||||
await userEvent.keyboard("{Escape}");
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
expect(screen.getByText("Share View")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
test("renders app tab when survey is app type and modalView is share", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockAppSurvey} modalView="share" />);
|
||||
|
||||
expect(screen.getByTestId("tab-container")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("app-tab")).toBeInTheDocument();
|
||||
expect(screen.getByText("In-app survey")).toBeInTheDocument();
|
||||
expect(screen.getByText("Display survey in your app")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders success view when survey is app type and modalView is start", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockAppSurvey} modalView="start" />);
|
||||
|
||||
expect(screen.getByTestId("success-view")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tab-container")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("sets correct width for dialog content based on survey type", () => {
|
||||
const { rerender } = render(<ShareSurveyModal {...defaultProps} survey={mockSurvey} />);
|
||||
|
||||
expect(screen.getByTestId("dialog-content")).toHaveAttribute("data-width", "wide");
|
||||
|
||||
rerender(<ShareSurveyModal {...defaultProps} survey={mockAppSurvey} />);
|
||||
|
||||
expect(screen.getByTestId("dialog-content")).toHaveAttribute("data-width", "default");
|
||||
});
|
||||
|
||||
test("generates correct tabs for link survey", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="start" />);
|
||||
|
||||
expect(screen.getByTestId("success-tab-anon-links")).toHaveTextContent("Share the link");
|
||||
expect(screen.getByTestId("success-tab-qr-code")).toHaveTextContent("QR Code");
|
||||
expect(screen.getByTestId("success-tab-personal-links")).toHaveTextContent("Personal links");
|
||||
expect(screen.getByTestId("success-tab-email")).toHaveTextContent("Embed in email");
|
||||
expect(screen.getByTestId("success-tab-website-embed")).toHaveTextContent("Embed on website");
|
||||
expect(screen.getByTestId("success-tab-dynamic-popup")).toHaveTextContent("Dynamic popup");
|
||||
});
|
||||
|
||||
test("shows single-use links label when singleUse is enabled", () => {
|
||||
const singleUseSurvey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
|
||||
render(<ShareSurveyModal {...defaultProps} survey={singleUseSurvey} modalView="start" />);
|
||||
|
||||
expect(screen.getByTestId("success-tab-anon-links")).toHaveTextContent("Single-use links");
|
||||
});
|
||||
|
||||
test("calls setOpen when dialog is closed", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ShareSurveyModal {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByTestId("dialog"));
|
||||
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("fetches survey URL on mount", async () => {
|
||||
const { getSurveyUrl } = await import("@/modules/analysis/utils");
|
||||
|
||||
render(<ShareSurveyModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getSurveyUrl).toHaveBeenCalledWith(mockSurvey, "https://example.com", "default");
|
||||
});
|
||||
});
|
||||
|
||||
describe("View switching functionality", () => {
|
||||
test("starts with SuccessView when modalView is 'start'", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="start" />);
|
||||
test("handles getSurveyUrl failure gracefully", async () => {
|
||||
const { getSurveyUrl } = await import("@/modules/analysis/utils");
|
||||
vi.mocked(getSurveyUrl).mockRejectedValue(new Error("Failed to fetch"));
|
||||
|
||||
expect(screen.getByTestId("success-view")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("share-view")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("starts with ShareView when modalView is 'share'", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="share" />);
|
||||
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("success-view")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("switches from SuccessView to ShareView when button is clicked", async () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="start" />);
|
||||
|
||||
expect(screen.getByTestId("success-view")).toBeInTheDocument();
|
||||
|
||||
const changeViewButton = screen.getByTestId("change-to-share-view");
|
||||
await userEvent.click(changeViewButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("success-view")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("switches to ShareView with specific tab when handleEmbedViewWithTab is called", async () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="start" />);
|
||||
|
||||
const embedButton = screen.getByTestId("embed-with-tab");
|
||||
await userEvent.click(embedButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("active-tab")).toHaveTextContent("email");
|
||||
});
|
||||
});
|
||||
// Render and verify it doesn't crash, even if nothing renders due to the error
|
||||
expect(() => {
|
||||
render(<ShareSurveyModal {...defaultProps} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
describe("Survey type specific behavior", () => {
|
||||
test("displays link survey tabs for link type survey", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="share" />);
|
||||
test("renders ShareView with correct active tab", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="share" />);
|
||||
|
||||
expect(screen.getByTestId("survey-type")).toHaveTextContent("link");
|
||||
expect(screen.getByTestId("tab-anon-links")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-personal-links")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-website-embed")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-email")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-social-media")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-qr-code")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-dynamic-popup")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays app survey tabs for app type survey", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockAppSurvey} modalView="share" />);
|
||||
|
||||
expect(screen.getByTestId("survey-type")).toHaveTextContent("app");
|
||||
expect(screen.getByTestId("tab-app")).toBeInTheDocument();
|
||||
|
||||
// Link-specific tabs should not be present for app surveys
|
||||
expect(screen.queryByTestId("tab-anonymous_links")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("tab-personal_links")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("sets correct default active tab based on survey type", () => {
|
||||
const linkSurveyRender = render(
|
||||
<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="share" />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.ANON_LINKS);
|
||||
|
||||
linkSurveyRender.unmount();
|
||||
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockAppSurvey} modalView="share" />);
|
||||
|
||||
expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.APP);
|
||||
});
|
||||
const shareViewData = screen.getByTestId("share-view-data");
|
||||
expect(shareViewData).toHaveTextContent("Active Tab: anon-links");
|
||||
});
|
||||
|
||||
describe("Tab switching functionality", () => {
|
||||
test("switches active tab when tab button is clicked", async () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="share" />);
|
||||
test("passes correct props to SuccessView", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="start" />);
|
||||
|
||||
expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.ANON_LINKS);
|
||||
|
||||
const emailTab = screen.getByTestId("tab-email");
|
||||
await userEvent.click(emailTab);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.EMAIL);
|
||||
});
|
||||
});
|
||||
const successViewData = screen.getByTestId("success-view-data");
|
||||
expect(successViewData).toHaveTextContent("Survey: test-survey-id");
|
||||
expect(successViewData).toHaveTextContent("Domain: https://example.com");
|
||||
expect(successViewData).toHaveTextContent("User: test-user-id");
|
||||
});
|
||||
|
||||
describe("Props passing", () => {
|
||||
test("passes correct props to SuccessView", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="start" />);
|
||||
test("resets to start view when modal is closed and reopened", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rerender } = render(<ShareSurveyModal {...defaultProps} modalView="share" />);
|
||||
|
||||
expect(screen.getByTestId("survey-id")).toHaveTextContent(mockLinkSurvey.id);
|
||||
});
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
|
||||
test("passes correct props to ShareView", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="share" />);
|
||||
rerender(<ShareSurveyModal {...defaultProps} modalView="share" open={false} />);
|
||||
rerender(<ShareSurveyModal {...defaultProps} modalView="share" open={true} />);
|
||||
|
||||
expect(screen.getByTestId("survey-type")).toHaveTextContent(mockLinkSurvey.type);
|
||||
expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.ANON_LINKS);
|
||||
});
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("URL handling", () => {
|
||||
test("initializes survey URL correctly", async () => {
|
||||
const { getSurveyUrl } = await import("@/modules/analysis/utils");
|
||||
const getSurveyUrlMock = vi.mocked(getSurveyUrl);
|
||||
test("sets correct active tab for link survey", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="share" />);
|
||||
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} />);
|
||||
|
||||
expect(getSurveyUrlMock).toHaveBeenCalledWith(mockLinkSurvey, defaultProps.publicDomain, "default");
|
||||
});
|
||||
expect(screen.getByTestId("share-view")).toHaveAttribute("data-active-id", "anon-links");
|
||||
});
|
||||
|
||||
describe("Effect handling", () => {
|
||||
test("updates showView when modalView prop changes", async () => {
|
||||
const { rerender } = render(
|
||||
<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="start" />
|
||||
);
|
||||
test("renders tab container for app survey in share mode", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} survey={mockAppSurvey} modalView="share" />);
|
||||
|
||||
expect(screen.getByTestId("success-view")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-container")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("app-tab")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("share-view")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
rerender(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="share" />);
|
||||
test("renders with contacts disabled", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="share" isContactsEnabled={false} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
// Just verify the ShareView renders correctly regardless of isContactsEnabled prop
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("share-view")).toHaveAttribute("data-active-id", "anon-links");
|
||||
});
|
||||
|
||||
test("updates showView when open prop changes", async () => {
|
||||
const { rerender } = render(
|
||||
<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="start" open={false} />
|
||||
);
|
||||
test("renders with formbricks cloud enabled", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="share" isFormbricksCloud={true} />);
|
||||
|
||||
expect(screen.queryByTestId("success-view")).not.toBeInTheDocument();
|
||||
// Just verify the ShareView renders correctly regardless of isFormbricksCloud prop
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
rerender(<ShareSurveyModal {...defaultProps} survey={mockLinkSurvey} modalView="start" open={true} />);
|
||||
test("correctly handles direct navigation to share view", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="share" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("success-view")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("success-view")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handler functions are passed to child components", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="start" />);
|
||||
|
||||
// Verify SuccessView receives the handler functions by checking buttons exist
|
||||
expect(screen.getByTestId("go-to-share-view")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("success-tab-anon-links")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("success-tab-qr-code")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("tab switching functionality is available in ShareView", () => {
|
||||
render(<ShareSurveyModal {...defaultProps} modalView="share" />);
|
||||
|
||||
// Verify ShareView has tab switching buttons
|
||||
expect(screen.getByTestId("tab-anon-links")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-qr-code")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-personal-links")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders different content based on survey type", () => {
|
||||
// Link survey renders ShareView
|
||||
const { rerender } = render(<ShareSurveyModal {...defaultProps} survey={mockSurvey} modalView="share" />);
|
||||
expect(screen.getByTestId("share-view")).toBeInTheDocument();
|
||||
|
||||
// App survey renders TabContainer with AppTab
|
||||
rerender(<ShareSurveyModal {...defaultProps} survey={mockAppSurvey} modalView="share" />);
|
||||
expect(screen.getByTestId("tab-container")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("app-tab")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("share-view")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
|
||||
import { AnonymousLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab";
|
||||
import { AppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab";
|
||||
import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab";
|
||||
@@ -19,9 +20,8 @@ import {
|
||||
MailIcon,
|
||||
QrCodeIcon,
|
||||
Share2Icon,
|
||||
SmartphoneIcon,
|
||||
SquareStack,
|
||||
UserIcon,
|
||||
UserIcon
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
@@ -161,17 +161,6 @@ export const ShareSurveyModal = ({
|
||||
]
|
||||
);
|
||||
|
||||
const appTabs = [
|
||||
{
|
||||
id: ShareViewType.APP,
|
||||
label: t("environments.surveys.share.embed_on_website.embed_in_app"),
|
||||
icon: SmartphoneIcon,
|
||||
title: t("environments.surveys.share.embed_on_website.embed_in_app"),
|
||||
componentType: AppTab,
|
||||
componentProps: {},
|
||||
},
|
||||
];
|
||||
|
||||
const [activeId, setActiveId] = useState(
|
||||
survey.type === "link" ? ShareViewType.ANON_LINKS : ShareViewType.APP
|
||||
);
|
||||
@@ -183,10 +172,10 @@ export const ShareSurveyModal = ({
|
||||
}, [open, modalView]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setActiveId(survey.type === "link" ? ShareViewType.ANON_LINKS : ShareViewType.APP);
|
||||
setOpen(open);
|
||||
if (!open) {
|
||||
setShowView("start");
|
||||
setActiveId(ShareViewType.ANON_LINKS);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -199,35 +188,54 @@ export const ShareSurveyModal = ({
|
||||
setActiveId(tabId);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (showView === "start") {
|
||||
return (
|
||||
<SuccessView
|
||||
survey={survey}
|
||||
surveyUrl={surveyUrl}
|
||||
publicDomain={publicDomain}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
user={user}
|
||||
tabs={linkTabs}
|
||||
handleViewChange={handleViewChange}
|
||||
handleEmbedViewWithTab={handleEmbedViewWithTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (survey.type === "link") {
|
||||
return (
|
||||
<ShareView
|
||||
tabs={linkTabs}
|
||||
activeId={activeId}
|
||||
setActiveId={setActiveId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`h-full w-full bg-slate-50 p-6 rounded-lg`}>
|
||||
<TabContainer
|
||||
title={t("environments.surveys.summary.in_app.title")}
|
||||
description={t("environments.surveys.summary.in_app.description")}>
|
||||
<AppTab />
|
||||
</TabContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<VisuallyHidden asChild>
|
||||
<DialogTitle />
|
||||
</VisuallyHidden>
|
||||
<DialogContent
|
||||
className="w-full overflow-y-auto bg-white p-0 lg:h-[700px]"
|
||||
width="wide"
|
||||
className="w-full bg-white p-0 lg:h-[700px]"
|
||||
width={survey.type === "link" ? "wide" : "default"}
|
||||
aria-describedby={undefined}
|
||||
unconstrained>
|
||||
{showView === "start" ? (
|
||||
<SuccessView
|
||||
survey={survey}
|
||||
surveyUrl={surveyUrl}
|
||||
publicDomain={publicDomain}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
user={user}
|
||||
tabs={linkTabs}
|
||||
handleViewChange={handleViewChange}
|
||||
handleEmbedViewWithTab={handleEmbedViewWithTab}
|
||||
/>
|
||||
) : (
|
||||
<ShareView
|
||||
tabs={survey.type === "link" ? linkTabs : appTabs}
|
||||
activeId={activeId}
|
||||
setActiveId={setActiveId}
|
||||
survey={survey}
|
||||
/>
|
||||
)}
|
||||
{renderContent()}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { MobileAppTab } from "./MobileAppTab";
|
||||
|
||||
// Mock @tolgee/react
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key, // Return the key itself for easy assertion
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock UI components
|
||||
vi.mock("@/modules/ui/components/alert", () => ({
|
||||
Alert: ({ children }: { children: React.ReactNode }) => <div data-testid="alert">{children}</div>,
|
||||
AlertTitle: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="alert-title">{children}</div>
|
||||
),
|
||||
AlertDescription: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="alert-description">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, asChild, ...props }: { children: React.ReactNode; asChild?: boolean }) =>
|
||||
asChild ? <div {...props}>{children}</div> : <button {...props}>{children}</button>,
|
||||
}));
|
||||
|
||||
// Mock next/link
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ children, href, target, ...props }: any) => (
|
||||
<a href={href} target={target} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("MobileAppTab", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders correctly with title, description, and learn more link", () => {
|
||||
render(<MobileAppTab />);
|
||||
|
||||
// Check for Alert component
|
||||
expect(screen.getByTestId("alert")).toBeInTheDocument();
|
||||
|
||||
// Check for AlertTitle with correct Tolgee key
|
||||
const alertTitle = screen.getByTestId("alert-title");
|
||||
expect(alertTitle).toBeInTheDocument();
|
||||
expect(alertTitle).toHaveTextContent("environments.surveys.summary.quickstart_mobile_apps");
|
||||
|
||||
// Check for AlertDescription with correct Tolgee key
|
||||
const alertDescription = screen.getByTestId("alert-description");
|
||||
expect(alertDescription).toBeInTheDocument();
|
||||
expect(alertDescription).toHaveTextContent(
|
||||
"environments.surveys.summary.quickstart_mobile_apps_description"
|
||||
);
|
||||
|
||||
// Check for the "Learn more" link
|
||||
const learnMoreLink = screen.getByRole("link", { name: "common.learn_more" });
|
||||
expect(learnMoreLink).toBeInTheDocument();
|
||||
expect(learnMoreLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides"
|
||||
);
|
||||
expect(learnMoreLink).toHaveAttribute("target", "_blank");
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import Link from "next/link";
|
||||
|
||||
export const MobileAppTab = () => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<Alert>
|
||||
<AlertTitle>{t("environments.surveys.summary.quickstart_mobile_apps")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.surveys.summary.quickstart_mobile_apps_description")}
|
||||
<Button asChild className="w-fit" size="sm" variant="link">
|
||||
<Link
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides"
|
||||
target="_blank">
|
||||
{t("common.learn_more")}
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { WebAppTab } from "./WebAppTab";
|
||||
|
||||
vi.mock("@/modules/ui/components/button/Button", () => ({
|
||||
Button: ({ children, onClick, ...props }: any) => (
|
||||
<button onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
CopyIcon: () => <div data-testid="copy-icon" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/alert", () => ({
|
||||
Alert: ({ children }: { children: React.ReactNode }) => <div data-testid="alert">{children}</div>,
|
||||
AlertTitle: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="alert-title">{children}</div>
|
||||
),
|
||||
AlertDescription: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="alert-description">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock navigator.clipboard.writeText
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: {
|
||||
writeText: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const surveyUrl = "https://app.formbricks.com/s/test-survey-id";
|
||||
const surveyId = "test-survey-id";
|
||||
|
||||
describe("WebAppTab", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders correctly with surveyUrl and surveyId", () => {
|
||||
render(<WebAppTab />);
|
||||
|
||||
expect(screen.getByText("environments.surveys.summary.quickstart_web_apps")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "common.learn_more" })).toHaveAttribute(
|
||||
"href",
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/quickstart"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import Link from "next/link";
|
||||
|
||||
export const WebAppTab = () => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<Alert>
|
||||
<AlertTitle>{t("environments.surveys.summary.quickstart_web_apps")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.surveys.summary.quickstart_web_apps_description")}
|
||||
<Button asChild className="w-fit" size="sm" variant="link">
|
||||
<Link
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/quickstart"
|
||||
target="_blank">
|
||||
{t("common.learn_more")}
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -205,128 +205,129 @@ export const AnonymousLinksTab = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full grow flex-col gap-6">
|
||||
<AdvancedOptionToggle
|
||||
htmlId="multi-use-link-switch"
|
||||
isChecked={isMultiUseLink}
|
||||
onToggle={handleMultiUseToggle}
|
||||
title={t("environments.surveys.share.anonymous_links.multi_use_link")}
|
||||
description={t("environments.surveys.share.anonymous_links.multi_use_link_description")}
|
||||
customContainerClass="p-0"
|
||||
childBorder>
|
||||
<div className="flex w-full flex-col gap-4 overflow-hidden bg-white p-4">
|
||||
<ShareSurveyLink
|
||||
survey={survey}
|
||||
surveyUrl={surveyUrl}
|
||||
publicDomain={publicDomain}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={locale}
|
||||
/>
|
||||
<div className="flex h-full flex-col justify-between space-y-4">
|
||||
<div className="flex w-full grow flex-col gap-6">
|
||||
<AdvancedOptionToggle
|
||||
htmlId="multi-use-link-switch"
|
||||
isChecked={isMultiUseLink}
|
||||
onToggle={handleMultiUseToggle}
|
||||
title={t("environments.surveys.share.anonymous_links.multi_use_link")}
|
||||
description={t("environments.surveys.share.anonymous_links.multi_use_link_description")}
|
||||
customContainerClass="pl-1 pr-0 py-0"
|
||||
childBorder>
|
||||
<div className="flex w-full flex-col gap-4 overflow-hidden bg-white p-4">
|
||||
<ShareSurveyLink
|
||||
survey={survey}
|
||||
surveyUrl={surveyUrl}
|
||||
publicDomain={publicDomain}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
<div className="w-full">
|
||||
<Alert variant="info" size="default">
|
||||
<AlertTitle>
|
||||
{t("environments.surveys.share.anonymous_links.multi_use_powers_other_channels_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
"environments.surveys.share.anonymous_links.multi_use_powers_other_channels_description"
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="w-full">
|
||||
<Alert variant="info" size="default">
|
||||
<AlertTitle>
|
||||
{t("environments.surveys.share.anonymous_links.multi_use_powers_other_channels_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
"environments.surveys.share.anonymous_links.multi_use_powers_other_channels_description"
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
</AdvancedOptionToggle>
|
||||
|
||||
<AdvancedOptionToggle
|
||||
htmlId="single-use-link-switch"
|
||||
isChecked={isSingleUseLink}
|
||||
onToggle={handleSingleUseToggle}
|
||||
title={t("environments.surveys.share.anonymous_links.single_use_link")}
|
||||
description={t("environments.surveys.share.anonymous_links.single_use_link_description")}
|
||||
customContainerClass="p-0"
|
||||
childBorder>
|
||||
<div className="flex w-full flex-col gap-4 bg-white p-4">
|
||||
<AdvancedOptionToggle
|
||||
htmlId="single-use-encryption-switch"
|
||||
isChecked={singleUseEncryption}
|
||||
onToggle={handleSingleUseEncryptionToggle}
|
||||
title={t("environments.surveys.share.anonymous_links.url_encryption_label")}
|
||||
description={t("environments.surveys.share.anonymous_links.url_encryption_description")}
|
||||
customContainerClass="p-0"
|
||||
/>
|
||||
<AdvancedOptionToggle
|
||||
htmlId="single-use-link-switch"
|
||||
isChecked={isSingleUseLink}
|
||||
onToggle={handleSingleUseToggle}
|
||||
title={t("environments.surveys.share.anonymous_links.single_use_link")}
|
||||
description={t("environments.surveys.share.anonymous_links.single_use_link_description")}
|
||||
customContainerClass="pl-1 pr-0 py-0"
|
||||
childBorder>
|
||||
<div className="flex w-full flex-col gap-4 bg-white p-4">
|
||||
<AdvancedOptionToggle
|
||||
htmlId="single-use-encryption-switch"
|
||||
isChecked={singleUseEncryption}
|
||||
onToggle={handleSingleUseEncryptionToggle}
|
||||
title={t("environments.surveys.share.anonymous_links.url_encryption_label")}
|
||||
description={t("environments.surveys.share.anonymous_links.url_encryption_description")}
|
||||
customContainerClass="pl-1 pr-0 py-0"
|
||||
/>
|
||||
|
||||
{!singleUseEncryption ? (
|
||||
<Alert variant="info" size="default">
|
||||
<AlertTitle>
|
||||
{t("environments.surveys.share.anonymous_links.custom_single_use_id_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.surveys.share.anonymous_links.custom_single_use_id_description")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{singleUseEncryption && (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<h3 className="text-sm font-medium text-slate-900">
|
||||
{t("environments.surveys.share.anonymous_links.number_of_links_label")}
|
||||
</h3>
|
||||
{!singleUseEncryption ? (
|
||||
<Alert variant="info" size="default">
|
||||
<AlertTitle>
|
||||
{t("environments.surveys.share.anonymous_links.custom_single_use_id_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.surveys.share.anonymous_links.custom_single_use_id_description")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{singleUseEncryption && (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="w-32">
|
||||
<Input
|
||||
type="number"
|
||||
max={5000}
|
||||
min={1}
|
||||
className="bg-white focus:border focus:border-slate-900"
|
||||
value={numberOfLinks}
|
||||
onChange={handleNumberOfLinksChange}
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-sm font-medium text-slate-900">
|
||||
{t("environments.surveys.share.anonymous_links.number_of_links_label")}
|
||||
</h3>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleGenerateLinks(Number(numberOfLinks) || 1)}
|
||||
disabled={Number(numberOfLinks) < 1 || Number(numberOfLinks) > 5000}>
|
||||
<div className="flex items-center gap-2">
|
||||
<CirclePlayIcon className="h-3.5 w-3.5 shrink-0 text-slate-50" />
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="w-32">
|
||||
<Input
|
||||
type="number"
|
||||
max={5000}
|
||||
min={1}
|
||||
className="bg-white focus:border focus:border-slate-900"
|
||||
value={numberOfLinks}
|
||||
onChange={handleNumberOfLinksChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-sm text-slate-50">
|
||||
{t("environments.surveys.share.anonymous_links.generate_and_download_links")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleGenerateLinks(Number(numberOfLinks) || 1)}
|
||||
disabled={Number(numberOfLinks) < 1 || Number(numberOfLinks) > 5000}>
|
||||
<div className="flex items-center gap-2">
|
||||
<CirclePlayIcon className="h-3.5 w-3.5 shrink-0 text-slate-50" />
|
||||
</div>
|
||||
|
||||
<span className="text-sm text-slate-50">
|
||||
{t("environments.surveys.share.anonymous_links.generate_and_download_links")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
)}
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
</div>
|
||||
|
||||
<DocumentationLinks
|
||||
links={[
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.single_use_links"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/single-use-links",
|
||||
},
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.data_prefilling"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/data-prefilling",
|
||||
},
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.source_tracking"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/source-tracking",
|
||||
},
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.custom_start_point"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-question",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DocumentationLinks
|
||||
links={[
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.single_use_links"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/single-use-links",
|
||||
},
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.data_prefilling"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/data-prefilling",
|
||||
},
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.source_tracking"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/source-tracking",
|
||||
},
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.custom_start_point"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-question",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{disableLinkModal && (
|
||||
<DisableLinkModal
|
||||
open={disableLinkModal.open}
|
||||
|
||||
@@ -1,63 +1,383 @@
|
||||
import { EnvironmentContextWrapper } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
||||
import { SurveyContextWrapper } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||
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 { TActionClass, TActionClassNoCodeConfig } from "@formbricks/types/action-classes";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TBaseFilter, TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
|
||||
import { AppTab } from "./app-tab";
|
||||
|
||||
vi.mock("@/modules/ui/components/options-switch", () => ({
|
||||
OptionsSwitch: (props: {
|
||||
options: Array<{ value: string; label: string }>;
|
||||
handleOptionChange: (value: string) => void;
|
||||
}) => (
|
||||
<div data-testid="options-switch">
|
||||
{props.options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
data-testid={`option-${option.value}`}
|
||||
onClick={() => props.handleOptionChange(option.value)}>
|
||||
{option.label}
|
||||
</button>
|
||||
// Mock Next.js Link component
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock DocumentationLinksSection
|
||||
vi.mock("./documentation-links-section", () => ({
|
||||
DocumentationLinksSection: ({ title, links }: { title: string; links: any[] }) => (
|
||||
<div data-testid="documentation-links">
|
||||
<h4>{title}</h4>
|
||||
{links.map((link) => (
|
||||
<div key={link.href} data-testid="documentation-link">
|
||||
<a href={link.href}>{link.title}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab",
|
||||
() => ({
|
||||
MobileAppTab: () => <div data-testid="mobile-app-tab">MobileAppTab</div>,
|
||||
})
|
||||
);
|
||||
// Mock segment
|
||||
const mockSegment: TSegment = {
|
||||
id: "test-segment-id",
|
||||
title: "Test Segment",
|
||||
description: "Test segment description",
|
||||
environmentId: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isPrivate: false,
|
||||
filters: [
|
||||
{
|
||||
id: "test-filter-id",
|
||||
connector: "and",
|
||||
resource: "contact",
|
||||
attributeKey: "test-attribute-key",
|
||||
attributeType: "string",
|
||||
condition: "equals",
|
||||
value: "test",
|
||||
} as unknown as TBaseFilter,
|
||||
],
|
||||
surveys: ["test-survey-id"],
|
||||
};
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab",
|
||||
() => ({
|
||||
WebAppTab: () => <div data-testid="web-app-tab">WebAppTab</div>,
|
||||
})
|
||||
);
|
||||
// Mock action class
|
||||
const mockActionClass: TActionClass = {
|
||||
id: "test-action-id",
|
||||
name: "Test Action",
|
||||
type: "code",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "test-env-id",
|
||||
description: "Test action description",
|
||||
noCodeConfig: null,
|
||||
key: "test-action-key",
|
||||
};
|
||||
|
||||
const mockNoCodeActionClass: TActionClass = {
|
||||
id: "test-no-code-action-id",
|
||||
name: "Test No Code Action",
|
||||
type: "noCode",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "test-env-id",
|
||||
description: "Test no code action description",
|
||||
noCodeConfig: {
|
||||
type: "click",
|
||||
elementSelector: {
|
||||
cssSelector: ".test-button",
|
||||
innerHtml: "Click me",
|
||||
},
|
||||
} as TActionClassNoCodeConfig,
|
||||
key: "test-no-code-action-key",
|
||||
};
|
||||
|
||||
// Mock environment data
|
||||
const mockEnvironment: TEnvironment = {
|
||||
id: "test-env-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "development",
|
||||
projectId: "test-project-id",
|
||||
appSetupCompleted: true,
|
||||
};
|
||||
|
||||
// Mock project data
|
||||
const mockProject = {
|
||||
id: "test-project-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
organizationId: "test-org-id",
|
||||
recontactDays: 7,
|
||||
config: {
|
||||
channel: "app",
|
||||
industry: "saas",
|
||||
},
|
||||
linkSurveyBranding: true,
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
brandColor: { light: "#ffffff", dark: "#000000" },
|
||||
questionColor: { light: "#000000", dark: "#ffffff" },
|
||||
inputColor: { light: "#000000", dark: "#ffffff" },
|
||||
inputBorderColor: { light: "#cccccc", dark: "#444444" },
|
||||
cardBackgroundColor: { light: "#ffffff", dark: "#000000" },
|
||||
cardBorderColor: { light: "#cccccc", dark: "#444444" },
|
||||
highlightBorderColor: { light: "#007bff", dark: "#0056b3" },
|
||||
isDarkModeEnabled: false,
|
||||
isLogoHidden: false,
|
||||
hideProgressBar: false,
|
||||
roundness: 8,
|
||||
cardArrangement: { linkSurveys: "casual", appSurveys: "casual" },
|
||||
},
|
||||
inAppSurveyBranding: true,
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
darkOverlay: false,
|
||||
logo: { url: "test-logo.png", bgColor: "#ffffff" },
|
||||
} as TProject;
|
||||
|
||||
// Mock survey data
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "test-survey-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
environmentId: "test-env-id",
|
||||
status: "inProgress",
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [{ actionClass: mockActionClass }],
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
welcomeCard: { enabled: false } as unknown as TSurveyWelcomeCard,
|
||||
questions: [],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false },
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
showLanguageSwitch: false,
|
||||
singleUse: { enabled: false, isEncrypted: false },
|
||||
projectOverwrites: null,
|
||||
surveyClosedMessage: null,
|
||||
delay: 0,
|
||||
isVerifyEmailEnabled: false,
|
||||
inlineTriggers: {},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
describe("AppTab", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders correctly by default with WebAppTab visible", () => {
|
||||
render(<AppTab />);
|
||||
expect(screen.getByTestId("options-switch")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("option-webapp")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("option-mobile")).toBeInTheDocument();
|
||||
const renderWithProviders = (appSetupCompleted = true, surveyOverrides = {}, projectOverrides = {}) => {
|
||||
const environmentWithSetup = {
|
||||
...mockEnvironment,
|
||||
appSetupCompleted,
|
||||
};
|
||||
|
||||
expect(screen.getByTestId("web-app-tab")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("mobile-app-tab")).not.toBeInTheDocument();
|
||||
const surveyWithOverrides = {
|
||||
...mockSurvey,
|
||||
...surveyOverrides,
|
||||
};
|
||||
|
||||
const projectWithOverrides = {
|
||||
...mockProject,
|
||||
...projectOverrides,
|
||||
};
|
||||
|
||||
return render(
|
||||
<EnvironmentContextWrapper environment={environmentWithSetup} project={projectWithOverrides}>
|
||||
<SurveyContextWrapper survey={surveyWithOverrides}>
|
||||
<AppTab />
|
||||
</SurveyContextWrapper>
|
||||
</EnvironmentContextWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
test("renders setup completed content when app setup is completed", () => {
|
||||
renderWithProviders(true);
|
||||
expect(screen.getByText("environments.surveys.summary.in_app.connection_title")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.connection_description")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("switches to MobileAppTab when mobile option is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AppTab />);
|
||||
test("renders setup required content when app setup is not completed", () => {
|
||||
renderWithProviders(false);
|
||||
expect(screen.getByText("environments.surveys.summary.in_app.no_connection_title")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.no_connection_description")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("common.connect_formbricks")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const mobileOptionButton = screen.getByTestId("option-mobile");
|
||||
await user.click(mobileOptionButton);
|
||||
test("displays correct wait time when survey has recontact days", () => {
|
||||
renderWithProviders(true, { recontactDays: 5 });
|
||||
expect(
|
||||
screen.getByText("5 environments.surveys.summary.in_app.display_criteria.time_based_days")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("(environments.surveys.summary.in_app.display_criteria.overwritten)")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("mobile-app-tab")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("web-app-tab")).not.toBeInTheDocument();
|
||||
test("displays correct wait time when survey has 1 recontact day", () => {
|
||||
renderWithProviders(true, { recontactDays: 1 });
|
||||
expect(
|
||||
screen.getByText("1 environments.surveys.summary.in_app.display_criteria.time_based_day")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("(environments.surveys.summary.in_app.display_criteria.overwritten)")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays correct wait time when survey has 0 recontact days", () => {
|
||||
renderWithProviders(true, { recontactDays: 0 });
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.time_based_always")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("(environments.surveys.summary.in_app.display_criteria.overwritten)")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays project recontact days when survey has no recontact days", () => {
|
||||
renderWithProviders(true, { recontactDays: null }, { recontactDays: 3 });
|
||||
expect(
|
||||
screen.getByText("3 environments.surveys.summary.in_app.display_criteria.time_based_days")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays always when project has 0 recontact days", () => {
|
||||
renderWithProviders(true, { recontactDays: null }, { recontactDays: 0 });
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.time_based_always")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays always when both survey and project have null recontact days", () => {
|
||||
renderWithProviders(true, { recontactDays: null }, { recontactDays: null });
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.time_based_always")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays correct display option for displayOnce", () => {
|
||||
renderWithProviders(true, { displayOption: "displayOnce" });
|
||||
expect(screen.getByText("environments.surveys.edit.show_only_once")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays correct display option for displayMultiple", () => {
|
||||
renderWithProviders(true, { displayOption: "displayMultiple" });
|
||||
expect(screen.getByText("environments.surveys.edit.until_they_submit_a_response")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays correct display option for respondMultiple", () => {
|
||||
renderWithProviders(true, { displayOption: "respondMultiple" });
|
||||
expect(
|
||||
screen.getByText("environments.surveys.edit.keep_showing_while_conditions_match")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays correct display option for displaySome", () => {
|
||||
renderWithProviders(true, { displayOption: "displaySome" });
|
||||
expect(screen.getByText("environments.surveys.edit.show_multiple_times")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays everyone when survey has no segment", () => {
|
||||
renderWithProviders(true, { segment: null });
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.everyone")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays targeted when survey has segment with filters", () => {
|
||||
renderWithProviders(true, {
|
||||
segment: mockSegment,
|
||||
});
|
||||
expect(screen.getByText("Test Segment")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays segment title when survey has public segment with filters", () => {
|
||||
const publicSegment = { ...mockSegment, isPrivate: false, title: "Public Segment" };
|
||||
renderWithProviders(true, {
|
||||
segment: publicSegment,
|
||||
});
|
||||
expect(screen.getByText("Public Segment")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays targeted when survey has private segment with filters", () => {
|
||||
const privateSegment = { ...mockSegment, isPrivate: true };
|
||||
renderWithProviders(true, {
|
||||
segment: privateSegment,
|
||||
});
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.targeted")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays everyone when survey has segment with no filters", () => {
|
||||
const emptySegment = { ...mockSegment, filters: [] };
|
||||
renderWithProviders(true, {
|
||||
segment: emptySegment,
|
||||
});
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.everyone")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays code trigger description correctly", () => {
|
||||
renderWithProviders(true, { triggers: [{ actionClass: mockActionClass }] });
|
||||
expect(screen.getByText("Test Action")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("(environments.surveys.summary.in_app.display_criteria.code_trigger)")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays no-code trigger description correctly", () => {
|
||||
renderWithProviders(true, { triggers: [{ actionClass: mockNoCodeActionClass }] });
|
||||
expect(screen.getByText("Test No Code Action")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"(environments.surveys.summary.in_app.display_criteria.no_code_trigger, environments.actions.click)"
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays randomizer when displayPercentage is set", () => {
|
||||
renderWithProviders(true, { displayPercentage: 25 });
|
||||
expect(
|
||||
screen.getAllByText(/environments\.surveys\.summary\.in_app\.display_criteria\.randomizer/)[0]
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not display randomizer when displayPercentage is null", () => {
|
||||
renderWithProviders(true, { displayPercentage: null });
|
||||
expect(screen.queryByText("Show to")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not display randomizer when displayPercentage is 0", () => {
|
||||
renderWithProviders(true, { displayPercentage: 0 });
|
||||
expect(screen.queryByText("Show to")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders documentation links section", () => {
|
||||
renderWithProviders(true);
|
||||
expect(screen.getByTestId("documentation-links")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.summary.in_app.documentation_title")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders all display criteria items", () => {
|
||||
renderWithProviders(true);
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.time_based_description")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.audience_description")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.trigger_description")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.summary.in_app.display_criteria.recontact_description")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,238 @@
|
||||
"use client";
|
||||
|
||||
import { MobileAppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab";
|
||||
import { WebAppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab";
|
||||
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
||||
import { useEnvironment } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
||||
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||
import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { H4, InlineSmall, Small } from "@/modules/ui/components/typography";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
CodeXmlIcon,
|
||||
MousePointerClickIcon,
|
||||
PercentIcon,
|
||||
Repeat1Icon,
|
||||
TimerResetIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { DocumentationLinksSection } from "./documentation-links-section";
|
||||
|
||||
export const AppTab = () => {
|
||||
const { t } = useTranslate();
|
||||
const [selectedTab, setSelectedTab] = useState("webapp");
|
||||
const createDocumentationLinks = (t: ReturnType<typeof useTranslate>["t"]) => [
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#html",
|
||||
title: t("environments.surveys.summary.in_app.html_embed"),
|
||||
},
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#react-js",
|
||||
title: t("environments.surveys.summary.in_app.javascript_sdk"),
|
||||
},
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#swift",
|
||||
title: t("environments.surveys.summary.in_app.ios_sdk"),
|
||||
},
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#android",
|
||||
title: t("environments.surveys.summary.in_app.kotlin_sdk"),
|
||||
},
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#react-native",
|
||||
title: t("environments.surveys.summary.in_app.react_native_sdk"),
|
||||
},
|
||||
];
|
||||
|
||||
const createNoCodeConfigType = (t: ReturnType<typeof useTranslate>["t"]) => ({
|
||||
click: t("environments.actions.click"),
|
||||
pageView: t("environments.actions.page_view"),
|
||||
exitIntent: t("environments.actions.exit_intent"),
|
||||
fiftyPercentScroll: t("environments.actions.fifty_percent_scroll"),
|
||||
});
|
||||
|
||||
const formatRecontactDaysString = (days: number, t: ReturnType<typeof useTranslate>["t"]) => {
|
||||
if (days === 0) {
|
||||
return t("environments.surveys.summary.in_app.display_criteria.time_based_always");
|
||||
} else if (days === 1) {
|
||||
return `${days} ${t("environments.surveys.summary.in_app.display_criteria.time_based_day")}`;
|
||||
} else {
|
||||
return `${days} ${t("environments.surveys.summary.in_app.display_criteria.time_based_days")}`;
|
||||
}
|
||||
};
|
||||
|
||||
interface DisplayCriteriaItemProps {
|
||||
icon: ReactNode;
|
||||
title: ReactNode;
|
||||
titleSuffix?: ReactNode;
|
||||
description: ReactNode;
|
||||
}
|
||||
|
||||
const DisplayCriteriaItem = ({ icon, title, titleSuffix, description }: DisplayCriteriaItemProps) => {
|
||||
return (
|
||||
<div className="flex h-full w-full grow flex-col">
|
||||
<OptionsSwitch
|
||||
options={[
|
||||
{ value: "webapp", label: t("environments.surveys.summary.web_app") },
|
||||
{ value: "mobile", label: t("environments.surveys.summary.mobile_app") },
|
||||
]}
|
||||
currentOption={selectedTab}
|
||||
handleOptionChange={(value) => setSelectedTab(value)}
|
||||
/>
|
||||
|
||||
<div className="mt-4">{selectedTab === "webapp" ? <WebAppTab /> : <MobileAppTab />}</div>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-x-2">
|
||||
<div className="flex items-center justify-center">{icon}</div>
|
||||
<div className="flex items-center">
|
||||
<Small>
|
||||
{title} {titleSuffix && <InlineSmall>{titleSuffix}</InlineSmall>}
|
||||
</Small>
|
||||
</div>
|
||||
<div />
|
||||
<div className="flex items-start">
|
||||
<Small color="muted" margin="headerDescription">
|
||||
{description}
|
||||
</Small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppTab = () => {
|
||||
const { t } = useTranslate();
|
||||
const { environment, project } = useEnvironment();
|
||||
const { survey } = useSurvey();
|
||||
|
||||
const documentationLinks = useMemo(() => createDocumentationLinks(t), [t]);
|
||||
const noCodeConfigType = useMemo(() => createNoCodeConfigType(t), [t]);
|
||||
|
||||
const waitTime = () => {
|
||||
if (survey.recontactDays !== null) {
|
||||
return formatRecontactDaysString(survey.recontactDays, t);
|
||||
}
|
||||
if (project.recontactDays !== null) {
|
||||
return formatRecontactDaysString(project.recontactDays, t);
|
||||
}
|
||||
return t("environments.surveys.summary.in_app.display_criteria.time_based_always");
|
||||
};
|
||||
|
||||
const displayOption = () => {
|
||||
if (survey.displayOption === "displayOnce") {
|
||||
return t("environments.surveys.edit.show_only_once");
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return t("environments.surveys.edit.until_they_submit_a_response");
|
||||
} else if (survey.displayOption === "respondMultiple") {
|
||||
return t("environments.surveys.edit.keep_showing_while_conditions_match");
|
||||
} else if (survey.displayOption === "displaySome") {
|
||||
return t("environments.surveys.edit.show_multiple_times");
|
||||
}
|
||||
|
||||
// Default fallback for undefined or unexpected displayOption values
|
||||
return t("environments.surveys.edit.show_only_once");
|
||||
};
|
||||
|
||||
const getTriggerDescription = (
|
||||
actionClass: TActionClass,
|
||||
noCodeConfigTypeParam: ReturnType<typeof createNoCodeConfigType>
|
||||
) => {
|
||||
if (actionClass.type === "code") {
|
||||
return `(${t("environments.surveys.summary.in_app.display_criteria.code_trigger")})`;
|
||||
} else {
|
||||
const configType = actionClass.noCodeConfig?.type;
|
||||
let configTypeLabel = "unknown";
|
||||
|
||||
if (configType && configType in noCodeConfigTypeParam) {
|
||||
configTypeLabel = noCodeConfigTypeParam[configType];
|
||||
} else if (configType) {
|
||||
configTypeLabel = configType;
|
||||
}
|
||||
|
||||
return `(${t("environments.surveys.summary.in_app.display_criteria.no_code_trigger")}, ${configTypeLabel})`;
|
||||
}
|
||||
};
|
||||
|
||||
const getSegmentTitle = (segment: TSegment | null) => {
|
||||
if (segment?.filters?.length && segment.filters.length > 0) {
|
||||
return segment.isPrivate
|
||||
? t("environments.surveys.summary.in_app.display_criteria.targeted")
|
||||
: segment.title;
|
||||
}
|
||||
return t("environments.surveys.summary.in_app.display_criteria.everyone");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between space-y-6 pb-4">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<Alert variant={environment.appSetupCompleted ? "success" : "warning"} size="default">
|
||||
<AlertTitle>
|
||||
{environment.appSetupCompleted
|
||||
? t("environments.surveys.summary.in_app.connection_title")
|
||||
: t("environments.surveys.summary.in_app.no_connection_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{environment.appSetupCompleted
|
||||
? t("environments.surveys.summary.in_app.connection_description")
|
||||
: t("environments.surveys.summary.in_app.no_connection_description")}
|
||||
</AlertDescription>
|
||||
{!environment.appSetupCompleted && (
|
||||
<AlertButton asChild>
|
||||
<Link href={`/environments/${environment.id}/project/app-connection`}>
|
||||
{t("common.connect_formbricks")}
|
||||
</Link>
|
||||
</AlertButton>
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
<div className="flex flex-col space-y-3">
|
||||
<H4>{t("environments.surveys.summary.in_app.display_criteria")}</H4>
|
||||
<div
|
||||
className={
|
||||
"flex w-full flex-col space-y-4 rounded-xl border border-slate-200 bg-white p-3 text-left shadow-sm"
|
||||
}>
|
||||
<DisplayCriteriaItem
|
||||
icon={<TimerResetIcon className="h-4 w-4" />}
|
||||
title={waitTime()}
|
||||
titleSuffix={
|
||||
survey.recontactDays !== null
|
||||
? `(${t("environments.surveys.summary.in_app.display_criteria.overwritten")})`
|
||||
: undefined
|
||||
}
|
||||
description={t("environments.surveys.summary.in_app.display_criteria.time_based_description")}
|
||||
/>
|
||||
<DisplayCriteriaItem
|
||||
icon={<UsersIcon className="h-4 w-4" />}
|
||||
title={getSegmentTitle(survey.segment)}
|
||||
description={t("environments.surveys.summary.in_app.display_criteria.audience_description")}
|
||||
/>
|
||||
{survey.triggers.map((trigger) => (
|
||||
<DisplayCriteriaItem
|
||||
key={trigger.actionClass.id}
|
||||
icon={
|
||||
trigger.actionClass.type === "code" ? (
|
||||
<CodeXmlIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<MousePointerClickIcon className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
title={trigger.actionClass.name}
|
||||
titleSuffix={getTriggerDescription(trigger.actionClass, noCodeConfigType)}
|
||||
description={t("environments.surveys.summary.in_app.display_criteria.trigger_description")}
|
||||
/>
|
||||
))}
|
||||
{survey.displayPercentage !== null && survey.displayPercentage > 0 && (
|
||||
<DisplayCriteriaItem
|
||||
icon={<PercentIcon className="h-4 w-4" />}
|
||||
title={t("environments.surveys.summary.in_app.display_criteria.randomizer", {
|
||||
percentage: survey.displayPercentage,
|
||||
})}
|
||||
description={t(
|
||||
"environments.surveys.summary.in_app.display_criteria.randomizer_description",
|
||||
{
|
||||
percentage: survey.displayPercentage,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<DisplayCriteriaItem
|
||||
icon={<Repeat1Icon className="h-4 w-4" />}
|
||||
title={displayOption()}
|
||||
description={t("environments.surveys.summary.in_app.display_criteria.recontact_description")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentationLinksSection
|
||||
title={t("environments.surveys.summary.in_app.documentation_title")}
|
||||
links={documentationLinks}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { H4 } from "@/modules/ui/components/typography";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface DocumentationLink {
|
||||
href: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface DocumentationLinksSectionProps {
|
||||
title: string;
|
||||
links: DocumentationLink[];
|
||||
}
|
||||
|
||||
export const DocumentationLinksSection = ({ title, links }: DocumentationLinksSectionProps) => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col space-y-3">
|
||||
<H4>{title}</H4>
|
||||
{links.map((link) => (
|
||||
<Alert key={link.title} size="small" variant="default">
|
||||
<ArrowUpRight className="size-4" />
|
||||
<AlertTitle>{link.title}</AlertTitle>
|
||||
<AlertButton>
|
||||
<Link href={link.href} target="_blank" rel="noopener noreferrer">
|
||||
{t("common.read_docs")}
|
||||
</Link>
|
||||
</AlertButton>
|
||||
</Alert>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { DocumentationLinksSection } from "./documentation-links-section";
|
||||
|
||||
// Mock the useTranslate hook
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => {
|
||||
if (key === "common.read_docs") {
|
||||
return "Read docs";
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock Next.js Link component
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock Alert components
|
||||
vi.mock("@/modules/ui/components/alert", () => ({
|
||||
Alert: ({ children, size, variant }: any) => (
|
||||
<div data-testid="alert" data-size={size} data-variant={variant}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
AlertButton: ({ children }: any) => <div data-testid="alert-button">{children}</div>,
|
||||
AlertTitle: ({ children }: any) => <div data-testid="alert-title">{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock Typography components
|
||||
vi.mock("@/modules/ui/components/typography", () => ({
|
||||
H4: ({ children }: any) => <h4 data-testid="h4">{children}</h4>,
|
||||
}));
|
||||
|
||||
// Mock lucide-react icons
|
||||
vi.mock("lucide-react", () => ({
|
||||
ArrowUpRight: ({ className }: any) => <svg data-testid="arrow-up-right-icon" className={className} />,
|
||||
}));
|
||||
|
||||
describe("DocumentationLinksSection", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockLinks = [
|
||||
{
|
||||
href: "https://example.com/docs/html",
|
||||
title: "HTML Documentation",
|
||||
},
|
||||
{
|
||||
href: "https://example.com/docs/react",
|
||||
title: "React Documentation",
|
||||
},
|
||||
{
|
||||
href: "https://example.com/docs/javascript",
|
||||
title: "JavaScript Documentation",
|
||||
},
|
||||
];
|
||||
|
||||
test("renders title correctly", () => {
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={mockLinks} />);
|
||||
|
||||
expect(screen.getByTestId("h4")).toHaveTextContent("Test Documentation Title");
|
||||
});
|
||||
|
||||
test("renders all documentation links", () => {
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={mockLinks} />);
|
||||
|
||||
expect(screen.getAllByTestId("alert")).toHaveLength(3);
|
||||
expect(screen.getByText("HTML Documentation")).toBeInTheDocument();
|
||||
expect(screen.getByText("React Documentation")).toBeInTheDocument();
|
||||
expect(screen.getByText("JavaScript Documentation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders links with correct href attributes", () => {
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={mockLinks} />);
|
||||
|
||||
const links = screen.getAllByRole("link");
|
||||
expect(links[0]).toHaveAttribute("href", "https://example.com/docs/html");
|
||||
expect(links[1]).toHaveAttribute("href", "https://example.com/docs/react");
|
||||
expect(links[2]).toHaveAttribute("href", "https://example.com/docs/javascript");
|
||||
});
|
||||
|
||||
test("renders links with correct target and rel attributes", () => {
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={mockLinks} />);
|
||||
|
||||
const links = screen.getAllByRole("link");
|
||||
links.forEach((link) => {
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
});
|
||||
|
||||
test("renders read docs button for each link", () => {
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={mockLinks} />);
|
||||
|
||||
const readDocsButtons = screen.getAllByText("Read docs");
|
||||
expect(readDocsButtons).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("renders icons for each alert", () => {
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={mockLinks} />);
|
||||
|
||||
const icons = screen.getAllByTestId("arrow-up-right-icon");
|
||||
expect(icons).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("renders alerts with correct props", () => {
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={mockLinks} />);
|
||||
|
||||
const alerts = screen.getAllByTestId("alert");
|
||||
alerts.forEach((alert) => {
|
||||
expect(alert).toHaveAttribute("data-size", "small");
|
||||
expect(alert).toHaveAttribute("data-variant", "default");
|
||||
});
|
||||
});
|
||||
|
||||
test("renders with empty links array", () => {
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={[]} />);
|
||||
|
||||
expect(screen.getByTestId("h4")).toHaveTextContent("Test Documentation Title");
|
||||
expect(screen.queryByTestId("alert")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders single link correctly", () => {
|
||||
const singleLink = [
|
||||
{
|
||||
href: "https://example.com/docs/single",
|
||||
title: "Single Documentation",
|
||||
},
|
||||
];
|
||||
|
||||
render(<DocumentationLinksSection title="Test Documentation Title" links={singleLink} />);
|
||||
|
||||
expect(screen.getAllByTestId("alert")).toHaveLength(1);
|
||||
expect(screen.getByText("Single Documentation")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link")).toHaveAttribute("href", "https://example.com/docs/single");
|
||||
});
|
||||
|
||||
test("renders with special characters in title and links", () => {
|
||||
const specialLinks = [
|
||||
{
|
||||
href: "https://example.com/docs/special?param=value&other=test",
|
||||
title: "Special Characters & Symbols",
|
||||
},
|
||||
];
|
||||
|
||||
render(<DocumentationLinksSection title="Special Title & Characters" links={specialLinks} />);
|
||||
|
||||
expect(screen.getByTestId("h4")).toHaveTextContent("Special Title & Characters");
|
||||
expect(screen.getByText("Special Characters & Symbols")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link")).toHaveAttribute(
|
||||
"href",
|
||||
"https://example.com/docs/special?param=value&other=test"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -21,34 +21,15 @@ vi.mock("@/modules/ui/components/alert", () => ({
|
||||
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", () => ({
|
||||
H3: (props: { children: React.ReactNode }) => <div data-testid="h3">{props.children}</div>,
|
||||
H4: (props: { children: React.ReactNode }) => <div data-testid="h4">{props.children}</div>,
|
||||
Small: (props: { children: React.ReactNode; color?: string; margin?: string }) => (
|
||||
<div data-testid="small" data-color={props.color} data-margin={props.margin}>
|
||||
{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
|
||||
// Mock DocumentationLinks
|
||||
vi.mock("./documentation-links", () => ({
|
||||
DocumentationLinks: (props: { links: Array<{ href: string; title: string }> }) => (
|
||||
<div data-testid="documentation-links">
|
||||
{props.links.map((link) => (
|
||||
<div key={link.title} data-testid="documentation-link" data-href={link.href} data-title={link.title}>
|
||||
{link.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -62,6 +43,12 @@ vi.mock("next/link", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("DynamicPopupTab", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -72,26 +59,31 @@ describe("DynamicPopupTab", () => {
|
||||
surveyId: "survey-123",
|
||||
};
|
||||
|
||||
test("renders with correct container structure", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const container = screen.getByTestId("dynamic-popup-container");
|
||||
expect(container).toHaveClass("flex", "h-full", "flex-col", "justify-between", "space-y-4");
|
||||
});
|
||||
|
||||
test("renders alert with correct props", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const alerts = screen.getAllByTestId("alert");
|
||||
const infoAlert = alerts.find((alert) => alert.getAttribute("data-variant") === "info");
|
||||
expect(infoAlert).toBeInTheDocument();
|
||||
expect(infoAlert).toHaveAttribute("data-variant", "info");
|
||||
expect(infoAlert).toHaveAttribute("data-size", "default");
|
||||
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", () => {
|
||||
test("renders alert title with correct translation key", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const alertTitles = screen.getAllByTestId("alert-title");
|
||||
const infoAlertTitle = alertTitles[0]; // The first one is the info alert
|
||||
expect(infoAlertTitle).toBeInTheDocument();
|
||||
expect(infoAlertTitle).toHaveTextContent("environments.surveys.share.dynamic_popup.alert_title");
|
||||
const alertTitle = screen.getByTestId("alert-title");
|
||||
expect(alertTitle).toBeInTheDocument();
|
||||
expect(alertTitle).toHaveTextContent("environments.surveys.share.dynamic_popup.alert_title");
|
||||
});
|
||||
|
||||
test("renders alert description with translation key", () => {
|
||||
test("renders alert description with correct translation key", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const alertDescription = screen.getByTestId("alert-description");
|
||||
@@ -102,85 +94,124 @@ describe("DynamicPopupTab", () => {
|
||||
test("renders alert button with link to survey edit page", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const alertButtons = screen.getAllByTestId("alert-button");
|
||||
const infoAlertButton = alertButtons[0]; // The first one is the info alert
|
||||
expect(infoAlertButton).toBeInTheDocument();
|
||||
expect(infoAlertButton).toHaveAttribute("data-as-child", "true");
|
||||
const alertButton = screen.getByTestId("alert-button");
|
||||
expect(alertButton).toBeInTheDocument();
|
||||
expect(alertButton).toHaveAttribute("data-as-child", "true");
|
||||
|
||||
const link = screen.getAllByTestId("next-link")[0];
|
||||
const link = screen.getByTestId("next-link");
|
||||
expect(link).toHaveAttribute("href", "/environments/env-123/surveys/survey-123/edit");
|
||||
expect(link).toHaveTextContent("environments.surveys.share.dynamic_popup.alert_button");
|
||||
});
|
||||
|
||||
test("renders attribute-based targeting documentation button", () => {
|
||||
test("renders DocumentationLinks component", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const links = screen.getAllByRole("link");
|
||||
const attributeLink = links.find((link) => link.getAttribute("href")?.includes("advanced-targeting"));
|
||||
|
||||
expect(attributeLink).toBeDefined();
|
||||
expect(attributeLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting"
|
||||
);
|
||||
expect(attributeLink).toHaveAttribute("target", "_blank");
|
||||
const documentationLinks = screen.getByTestId("documentation-links");
|
||||
expect(documentationLinks).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders code and no code triggers documentation button", () => {
|
||||
test("passes correct documentation links to DocumentationLinks component", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const links = screen.getAllByRole("link");
|
||||
const actionsLink = links.find((link) => link.getAttribute("href")?.includes("actions"));
|
||||
|
||||
expect(actionsLink).toBeDefined();
|
||||
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.getAllByRole("link");
|
||||
const recontactLink = links.find((link) => link.getAttribute("href")?.includes("recontact"));
|
||||
|
||||
expect(recontactLink).toBeDefined();
|
||||
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 links = screen.getAllByRole("link");
|
||||
const documentationLinks = links.filter(
|
||||
(link) =>
|
||||
link.getAttribute("href")?.includes("formbricks.com/docs") && link.getAttribute("target") === "_blank"
|
||||
);
|
||||
|
||||
// There are 3 unique documentation URLs
|
||||
const documentationLinks = screen.getAllByTestId("documentation-link");
|
||||
expect(documentationLinks).toHaveLength(3);
|
||||
|
||||
documentationLinks.forEach((link) => {
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
});
|
||||
});
|
||||
|
||||
test("documentation button links open in new tab", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const links = screen.getAllByRole("link");
|
||||
const documentationLinks = links.filter((link) =>
|
||||
link.getAttribute("href")?.includes("formbricks.com/docs")
|
||||
// Check attribute-based targeting link
|
||||
const attributeLink = documentationLinks.find(
|
||||
(link) =>
|
||||
link.getAttribute("data-href") ===
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting"
|
||||
);
|
||||
expect(attributeLink).toBeInTheDocument();
|
||||
expect(attributeLink).toHaveAttribute(
|
||||
"data-title",
|
||||
"environments.surveys.share.dynamic_popup.attribute_based_targeting"
|
||||
);
|
||||
|
||||
documentationLinks.forEach((link) => {
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
// Check code and no code triggers link
|
||||
const actionsLink = documentationLinks.find(
|
||||
(link) =>
|
||||
link.getAttribute("data-href") ===
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions"
|
||||
);
|
||||
expect(actionsLink).toBeInTheDocument();
|
||||
expect(actionsLink).toHaveAttribute(
|
||||
"data-title",
|
||||
"environments.surveys.share.dynamic_popup.code_no_code_triggers"
|
||||
);
|
||||
|
||||
// Check recontact options link
|
||||
const recontactLink = documentationLinks.find(
|
||||
(link) =>
|
||||
link.getAttribute("data-href") ===
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/recontact"
|
||||
);
|
||||
expect(recontactLink).toBeInTheDocument();
|
||||
expect(recontactLink).toHaveAttribute(
|
||||
"data-title",
|
||||
"environments.surveys.share.dynamic_popup.recontact_options"
|
||||
);
|
||||
});
|
||||
|
||||
test("renders documentation links with correct titles", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const documentationLinks = screen.getAllByTestId("documentation-link");
|
||||
|
||||
const expectedTitles = [
|
||||
"environments.surveys.share.dynamic_popup.attribute_based_targeting",
|
||||
"environments.surveys.share.dynamic_popup.code_no_code_triggers",
|
||||
"environments.surveys.share.dynamic_popup.recontact_options",
|
||||
];
|
||||
|
||||
expectedTitles.forEach((title) => {
|
||||
const link = documentationLinks.find((link) => link.getAttribute("data-title") === title);
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveTextContent(title);
|
||||
});
|
||||
});
|
||||
|
||||
test("renders documentation links with correct URLs", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
const documentationLinks = screen.getAllByTestId("documentation-link");
|
||||
|
||||
const expectedUrls = [
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting",
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions",
|
||||
"https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/recontact",
|
||||
];
|
||||
|
||||
expectedUrls.forEach((url) => {
|
||||
const link = documentationLinks.find((link) => link.getAttribute("data-href") === url);
|
||||
expect(link).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("calls translation function for all text content", () => {
|
||||
render(<DynamicPopupTab {...defaultProps} />);
|
||||
|
||||
// Check alert translations
|
||||
expect(screen.getByTestId("alert-title")).toHaveTextContent(
|
||||
"environments.surveys.share.dynamic_popup.alert_title"
|
||||
);
|
||||
expect(screen.getByTestId("alert-description")).toHaveTextContent(
|
||||
"environments.surveys.share.dynamic_popup.alert_description"
|
||||
);
|
||||
expect(screen.getByTestId("next-link")).toHaveTextContent(
|
||||
"environments.surveys.share.dynamic_popup.alert_button"
|
||||
);
|
||||
});
|
||||
|
||||
test("renders with correct props when environmentId and surveyId change", () => {
|
||||
const newProps = {
|
||||
environmentId: "env-456",
|
||||
surveyId: "survey-456",
|
||||
};
|
||||
|
||||
render(<DynamicPopupTab {...newProps} />);
|
||||
|
||||
const link = screen.getByTestId("next-link");
|
||||
expect(link).toHaveAttribute("href", "/environments/env-456/surveys/survey-456/edit");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ export const DynamicPopupTab = ({ environmentId, surveyId }: DynamicPopupTabProp
|
||||
const { t } = useTranslate();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-between space-y-4">
|
||||
<div className="flex h-full flex-col justify-between space-y-4" data-testid="dynamic-popup-container">
|
||||
<Alert variant="info" size="default">
|
||||
<AlertTitle>{t("environments.surveys.share.dynamic_popup.alert_title")}</AlertTitle>
|
||||
<AlertDescription>{t("environments.surveys.share.dynamic_popup.alert_description")}</AlertDescription>
|
||||
|
||||
@@ -167,75 +167,74 @@ export const PersonalLinksTab = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<div className="flex h-full grow flex-col gap-6">
|
||||
{/* Recipients Section */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="selectedSegment"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.recipients")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
disabled={publicSegments.length === 0}>
|
||||
<SelectTrigger className="w-full bg-white">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
publicSegments.length === 0
|
||||
? t("environments.surveys.share.personal_links.no_segments_available")
|
||||
: t("environments.surveys.share.personal_links.select_segment")
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{publicSegments.map((segment) => (
|
||||
<SelectItem key={segment.id} value={segment.id}>
|
||||
{segment.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.share.personal_links.create_and_manage_segments")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex h-full flex-col justify-between space-y-4">
|
||||
<FormProvider {...form}>
|
||||
<div className="flex grow flex-col gap-6">
|
||||
{/* Recipients Section */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="selectedSegment"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.recipients")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
disabled={publicSegments.length === 0}>
|
||||
<SelectTrigger className="w-full bg-white">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
publicSegments.length === 0
|
||||
? t("environments.surveys.share.personal_links.no_segments_available")
|
||||
: t("environments.surveys.share.personal_links.select_segment")
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{publicSegments.map((segment) => (
|
||||
<SelectItem key={segment.id} value={segment.id}>
|
||||
{segment.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.share.personal_links.create_and_manage_segments")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Expiry Date Section */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expiryDate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("environments.surveys.share.personal_links.expiry_date_optional")}</FormLabel>
|
||||
<FormControl>
|
||||
<RestrictedDatePicker date={field.value} updateSurveyDate={field.onChange} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.share.personal_links.expiry_date_description")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Expiry Date Section */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expiryDate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("environments.surveys.share.personal_links.expiry_date_optional")}</FormLabel>
|
||||
<FormControl>
|
||||
<RestrictedDatePicker date={field.value} updateSurveyDate={field.onChange} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.share.personal_links.expiry_date_description")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Generate Button */}
|
||||
<Button
|
||||
onClick={handleGenerateLinks}
|
||||
disabled={isButtonDisabled}
|
||||
loading={isGenerating}
|
||||
className="w-fit">
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
{/* Info Box */}
|
||||
{/* Generate Button */}
|
||||
<Button
|
||||
onClick={handleGenerateLinks}
|
||||
disabled={isButtonDisabled}
|
||||
loading={isGenerating}
|
||||
className="w-fit">
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</FormProvider>
|
||||
<DocumentationLinks
|
||||
links={[
|
||||
{
|
||||
@@ -244,6 +243,6 @@ export const PersonalLinksTab = ({
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,11 +52,7 @@ vi.mock("@/modules/ui/components/sidebar", () => ({
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
vi.mock("./app-tab", () => ({
|
||||
AppTab: () => <div data-testid="app-tab">AppTab Content</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./email-tab", () => ({
|
||||
vi.mock("./EmailTab", () => ({
|
||||
EmailTab: (props: { surveyId: string; email: string }) => (
|
||||
<div data-testid="email-tab">
|
||||
EmailTab Content for {props.surveyId} with {props.email}
|
||||
@@ -64,12 +60,25 @@ vi.mock("./email-tab", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./anonymous-links-tab", () => ({
|
||||
AnonymousLinksTab: (props: {
|
||||
survey: TSurvey;
|
||||
surveyUrl: string;
|
||||
publicDomain: string;
|
||||
setSurveyUrl: (url: string) => void;
|
||||
locale: TUserLocale;
|
||||
}) => (
|
||||
<div data-testid="anonymous-links-tab">
|
||||
AnonymousLinksTab Content for {props.survey.id} at {props.surveyUrl}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./qr-code-tab", () => ({
|
||||
QRCodeTab: (props: { surveyUrl: string }) => (
|
||||
<div data-testid="qr-code-tab">QRCodeTab Content for {props.surveyUrl}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./website-embed-tab", () => ({
|
||||
WebsiteEmbedTab: (props: { surveyUrl: string }) => (
|
||||
<div data-testid="website-embed-tab">WebsiteEmbedTab Content for {props.surveyUrl}</div>
|
||||
@@ -83,7 +92,6 @@ vi.mock("./dynamic-popup-tab", () => ({
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./tab-container", () => ({
|
||||
TabContainer: (props: { children: React.ReactNode; title: string; description: string }) => (
|
||||
<div data-testid="tab-container">
|
||||
@@ -102,16 +110,10 @@ vi.mock("./personal-links-tab", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./anonymous-links-tab", () => ({
|
||||
AnonymousLinksTab: (props: {
|
||||
survey: TSurvey;
|
||||
surveyUrl: string;
|
||||
publicDomain: string;
|
||||
setSurveyUrl: (url: string) => void;
|
||||
locale: TUserLocale;
|
||||
}) => (
|
||||
<div data-testid="anonymous-links-tab">
|
||||
AnonymousLinksTab Content for {props.survey.id} at {props.surveyUrl}
|
||||
vi.mock("./social-media-tab", () => ({
|
||||
SocialMediaTab: (props: { surveyUrl: string; surveyTitle: string }) => (
|
||||
<div data-testid="social-media-tab">
|
||||
SocialMediaTab Content for {props.surveyTitle} at {props.surveyUrl}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -154,6 +156,11 @@ vi.mock("lucide-react", () => ({
|
||||
Download
|
||||
</div>
|
||||
),
|
||||
Code2Icon: () => <div data-testid="code2-icon">Code2Icon</div>,
|
||||
QrCodeIcon: () => <div data-testid="qr-code-icon">QrCodeIcon</div>,
|
||||
Share2Icon: () => <div data-testid="share2-icon">Share2Icon</div>,
|
||||
SquareStack: () => <div data-testid="square-stack-icon">SquareStack</div>,
|
||||
UserIcon: () => <div data-testid="user-icon">UserIcon</div>,
|
||||
}));
|
||||
|
||||
// Mock tooltip and typography components
|
||||
@@ -189,128 +196,150 @@ vi.mock("@/lib/cn", () => ({
|
||||
cn: (...args: any[]) => args.filter(Boolean).join(" "),
|
||||
}));
|
||||
|
||||
const mockTabs: Array<{
|
||||
id: ShareViewType;
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
componentType: React.ComponentType<any>;
|
||||
componentProps: any;
|
||||
title: string;
|
||||
description?: string;
|
||||
}> = [
|
||||
// Mock i18n
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock component imports for tabs
|
||||
const MockEmailTab = ({ surveyId, email }: { surveyId: string; email: string }) => (
|
||||
<div data-testid="email-tab">
|
||||
EmailTab Content for {surveyId} with {email}
|
||||
</div>
|
||||
);
|
||||
|
||||
const MockAnonymousLinksTab = ({ survey, surveyUrl }: { survey: any; surveyUrl: string }) => (
|
||||
<div data-testid="anonymous-links-tab">
|
||||
AnonymousLinksTab Content for {survey.id} at {surveyUrl}
|
||||
</div>
|
||||
);
|
||||
|
||||
const MockWebsiteEmbedTab = ({ surveyUrl }: { surveyUrl: string }) => (
|
||||
<div data-testid="website-embed-tab">WebsiteEmbedTab Content for {surveyUrl}</div>
|
||||
);
|
||||
|
||||
const MockDynamicPopupTab = ({ environmentId, surveyId }: { environmentId: string; surveyId: string }) => (
|
||||
<div data-testid="dynamic-popup-tab">
|
||||
DynamicPopupTab Content for {surveyId} in {environmentId}
|
||||
</div>
|
||||
);
|
||||
|
||||
const MockQRCodeTab = ({ surveyUrl }: { surveyUrl: string }) => (
|
||||
<div data-testid="qr-code-tab">QRCodeTab Content for {surveyUrl}</div>
|
||||
);
|
||||
|
||||
const MockPersonalLinksTab = ({ surveyId, environmentId }: { surveyId: string; environmentId: string }) => (
|
||||
<div data-testid="personal-links-tab">
|
||||
PersonalLinksTab Content for {surveyId} in {environmentId}
|
||||
</div>
|
||||
);
|
||||
|
||||
const MockSocialMediaTab = ({ surveyUrl, surveyTitle }: { surveyUrl: string; surveyTitle: string }) => (
|
||||
<div data-testid="social-media-tab">
|
||||
SocialMediaTab Content for {surveyTitle} at {surveyUrl}
|
||||
</div>
|
||||
);
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey1",
|
||||
type: "link",
|
||||
name: "Test Survey",
|
||||
status: "inProgress",
|
||||
environmentId: "env1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
questions: [],
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: 0,
|
||||
triggers: [],
|
||||
languages: [],
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
singleUse: { enabled: false, isEncrypted: false },
|
||||
styling: null,
|
||||
} as any;
|
||||
|
||||
const mockTabs = [
|
||||
{
|
||||
id: ShareViewType.EMAIL,
|
||||
label: "Email",
|
||||
icon: () => <div data-testid="email-tab-icon" />,
|
||||
componentType: () => <div data-testid="email-tab-content">Email Content</div>,
|
||||
componentProps: {},
|
||||
title: "Email",
|
||||
description: "Email Description",
|
||||
componentType: MockEmailTab,
|
||||
componentProps: { surveyId: "survey1", email: "test@example.com" },
|
||||
title: "Send Email",
|
||||
description: "Send survey via email",
|
||||
},
|
||||
{
|
||||
id: ShareViewType.WEBSITE_EMBED,
|
||||
label: "Website Embed",
|
||||
icon: () => <div data-testid="website-embed-tab-icon" />,
|
||||
componentType: () => <div data-testid="website-embed-tab-content">Website Embed Content</div>,
|
||||
componentProps: {},
|
||||
title: "Website Embed",
|
||||
description: "Website Embed Description",
|
||||
componentType: MockWebsiteEmbedTab,
|
||||
componentProps: { surveyUrl: "http://example.com/survey1" },
|
||||
title: "Embed on Website",
|
||||
description: "Embed survey on your website",
|
||||
},
|
||||
{
|
||||
id: ShareViewType.DYNAMIC_POPUP,
|
||||
label: "Dynamic Popup",
|
||||
icon: () => <div data-testid="dynamic-popup-tab-icon" />,
|
||||
componentType: () => <div data-testid="dynamic-popup-tab-content">Dynamic Popup Content</div>,
|
||||
componentProps: {},
|
||||
componentType: MockDynamicPopupTab,
|
||||
componentProps: { environmentId: "env1", surveyId: "survey1" },
|
||||
title: "Dynamic Popup",
|
||||
description: "Dynamic Popup Description",
|
||||
description: "Show survey as popup",
|
||||
},
|
||||
{
|
||||
id: ShareViewType.ANON_LINKS,
|
||||
label: "Anonymous Links",
|
||||
icon: () => <div data-testid="link-tab-icon" />,
|
||||
componentType: () => <div data-testid="anonymous-links-tab-content">Anonymous Links Content</div>,
|
||||
componentProps: {},
|
||||
icon: () => <div data-testid="anonymous-links-tab-icon" />,
|
||||
componentType: MockAnonymousLinksTab,
|
||||
componentProps: {
|
||||
survey: mockSurvey,
|
||||
surveyUrl: "http://example.com/survey1",
|
||||
publicDomain: "http://example.com",
|
||||
setSurveyUrl: vi.fn(),
|
||||
locale: "en" as any,
|
||||
},
|
||||
title: "Anonymous Links",
|
||||
description: "Anonymous Links Description",
|
||||
description: "Share anonymous links",
|
||||
},
|
||||
{
|
||||
id: ShareViewType.QR_CODE,
|
||||
label: "QR Code",
|
||||
icon: () => <div data-testid="qr-code-tab-icon" />,
|
||||
componentType: () => <div data-testid="qr-code-tab-content">QR Code Content</div>,
|
||||
componentProps: {},
|
||||
componentType: MockQRCodeTab,
|
||||
componentProps: { surveyUrl: "http://example.com/survey1" },
|
||||
title: "QR Code",
|
||||
description: "QR Code Description",
|
||||
description: "Generate QR code",
|
||||
},
|
||||
{
|
||||
id: ShareViewType.APP,
|
||||
label: "App",
|
||||
icon: () => <div data-testid="app-tab-icon" />,
|
||||
componentType: () => <div data-testid="app-tab-content">App Content</div>,
|
||||
componentProps: {},
|
||||
title: "App",
|
||||
description: "App Description",
|
||||
id: ShareViewType.PERSONAL_LINKS,
|
||||
label: "Personal Links",
|
||||
icon: () => <div data-testid="personal-links-tab-icon" />,
|
||||
componentType: MockPersonalLinksTab,
|
||||
componentProps: { surveyId: "survey1", environmentId: "env1" },
|
||||
title: "Personal Links",
|
||||
description: "Create personal links",
|
||||
},
|
||||
{
|
||||
id: ShareViewType.SOCIAL_MEDIA,
|
||||
label: "Social Media",
|
||||
icon: () => <div data-testid="social-media-tab-icon" />,
|
||||
componentType: MockSocialMediaTab,
|
||||
componentProps: { surveyUrl: "http://example.com/survey1", surveyTitle: "Test Survey" },
|
||||
title: "Social Media",
|
||||
description: "Share on social media",
|
||||
},
|
||||
];
|
||||
|
||||
const mockSurveyLink = {
|
||||
id: "survey1",
|
||||
type: "link",
|
||||
name: "Test Link Survey",
|
||||
status: "inProgress",
|
||||
environmentId: "env1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
questions: [],
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: 0,
|
||||
triggers: [],
|
||||
languages: [],
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
singleUse: { enabled: false, isEncrypted: false },
|
||||
styling: null,
|
||||
} as any;
|
||||
const mockSurveyWeb = {
|
||||
id: "survey2",
|
||||
type: "app",
|
||||
name: "Test Web Survey",
|
||||
status: "inProgress",
|
||||
environmentId: "env1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
questions: [],
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: 0,
|
||||
triggers: [],
|
||||
languages: [],
|
||||
autoClose: null,
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
singleUse: { enabled: false, isEncrypted: false },
|
||||
styling: null,
|
||||
} as any;
|
||||
|
||||
const defaultProps = {
|
||||
tabs: mockTabs,
|
||||
activeId: ShareViewType.EMAIL,
|
||||
setActiveId: vi.fn(),
|
||||
environmentId: "env1",
|
||||
survey: mockSurveyLink,
|
||||
email: "test@example.com",
|
||||
surveyUrl: "http://example.com/survey1",
|
||||
publicDomain: "http://example.com",
|
||||
setSurveyUrl: vi.fn(),
|
||||
locale: "en" as any,
|
||||
segments: [],
|
||||
isContactsEnabled: true,
|
||||
isFormbricksCloud: false,
|
||||
};
|
||||
|
||||
// Mock window object for resize testing
|
||||
@@ -332,33 +361,99 @@ describe("ShareView", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("does not render desktop tabs for non-link survey type", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyWeb} />);
|
||||
test("renders sidebar with tabs", () => {
|
||||
render(<ShareView {...defaultProps} />);
|
||||
|
||||
// For non-link survey types, desktop sidebar should not be rendered
|
||||
// Check that SidebarProvider is not rendered by looking for sidebar-specific elements
|
||||
const sidebarLabel = screen.queryByText("Share via");
|
||||
expect(sidebarLabel).toBeNull();
|
||||
// Sidebar should always be rendered
|
||||
const sidebarLabel = screen.getByText("environments.surveys.share.share_view_title");
|
||||
expect(sidebarLabel).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders desktop tabs for link survey type", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} activeId={ShareViewType.EMAIL} />);
|
||||
test("renders desktop tabs", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
// For link survey types, desktop sidebar should be rendered
|
||||
// Desktop sidebar should be rendered
|
||||
const sidebarLabel = screen.getByText("environments.surveys.share.share_view_title");
|
||||
expect(sidebarLabel).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls setActiveId when a tab is clicked (desktop)", async () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} activeId={ShareViewType.EMAIL} />);
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
const websiteEmbedTabButton = screen.getByLabelText("Website Embed");
|
||||
await userEvent.click(websiteEmbedTabButton);
|
||||
expect(defaultProps.setActiveId).toHaveBeenCalledWith(ShareViewType.WEBSITE_EMBED);
|
||||
});
|
||||
|
||||
test("renders EmailTab when activeId is EMAIL", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.EMAIL} />);
|
||||
expect(screen.getByTestId("email-tab")).toBeInTheDocument();
|
||||
expect(screen.getByText("EmailTab Content for survey1 with test@example.com")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders WebsiteEmbedTab when activeId is WEBSITE_EMBED", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.WEBSITE_EMBED} />);
|
||||
expect(screen.getByTestId("tab-container")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("website-embed-tab")).toBeInTheDocument();
|
||||
expect(screen.getByText("WebsiteEmbedTab Content for http://example.com/survey1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders DynamicPopupTab when activeId is DYNAMIC_POPUP", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.DYNAMIC_POPUP} />);
|
||||
expect(screen.getByTestId("tab-container")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dynamic-popup-tab")).toBeInTheDocument();
|
||||
expect(screen.getByText("DynamicPopupTab Content for survey1 in env1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders AnonymousLinksTab when activeId is ANON_LINKS", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.ANON_LINKS} />);
|
||||
expect(screen.getByTestId("anonymous-links-tab")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("AnonymousLinksTab Content for survey1 at http://example.com/survey1")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders QRCodeTab when activeId is QR_CODE", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.QR_CODE} />);
|
||||
expect(screen.getByTestId("qr-code-tab")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders nothing when activeId doesn't match any tab", () => {
|
||||
// Create a special case with no matching tab
|
||||
const propsWithNoMatchingTab = {
|
||||
...defaultProps,
|
||||
tabs: mockTabs.slice(0, 3), // Only include first 3 tabs
|
||||
activeId: ShareViewType.SOCIAL_MEDIA, // Use a tab not in the subset
|
||||
};
|
||||
|
||||
render(<ShareView {...propsWithNoMatchingTab} />);
|
||||
|
||||
// Should not render any tab content for non-matching activeId
|
||||
expect(screen.queryByTestId("email-tab")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("website-embed-tab")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dynamic-popup-tab")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("anonymous-links-tab")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("qr-code-tab")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("personal-links-tab")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("social-media-tab")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders PersonalLinksTab when activeId is PERSONAL_LINKS", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.PERSONAL_LINKS} />);
|
||||
expect(screen.getByTestId("personal-links-tab")).toBeInTheDocument();
|
||||
expect(screen.getByText("PersonalLinksTab Content for survey1 in env1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders SocialMediaTab when activeId is SOCIAL_MEDIA", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.SOCIAL_MEDIA} />);
|
||||
expect(screen.getByTestId("social-media-tab")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("SocialMediaTab Content for Test Survey at http://example.com/survey1")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls setActiveId when a responsive tab is clicked", async () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} activeId={ShareViewType.EMAIL} />);
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
// Get responsive buttons - these are Button components containing icons
|
||||
const responsiveButtons = screen.getAllByTestId("website-embed-tab-icon");
|
||||
@@ -377,7 +472,7 @@ describe("ShareView", () => {
|
||||
});
|
||||
|
||||
test("applies active styles to the active tab (desktop)", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} activeId={ShareViewType.EMAIL} />);
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
const emailTabButton = screen.getByLabelText("Email");
|
||||
expect(emailTabButton).toHaveClass("bg-slate-100");
|
||||
@@ -390,7 +485,7 @@ describe("ShareView", () => {
|
||||
});
|
||||
|
||||
test("applies active styles to the active tab (responsive)", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} activeId={ShareViewType.EMAIL} />);
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
// Get responsive buttons - these are Button components with ghost variant
|
||||
const responsiveButtons = screen.getAllByTestId("email-tab-icon");
|
||||
@@ -421,268 +516,36 @@ describe("ShareView", () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe("Responsive Behavior", () => {
|
||||
test("detects large screen size on mount", () => {
|
||||
window.innerWidth = 1200;
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
test("renders all tabs from props", () => {
|
||||
render(<ShareView {...defaultProps} />);
|
||||
|
||||
// SidebarProvider should be rendered with open=true for large screens
|
||||
const sidebarProvider = screen.getByTestId("sidebar-provider");
|
||||
expect(sidebarProvider).toHaveAttribute("data-open", "true");
|
||||
});
|
||||
|
||||
test("detects small screen size on mount", () => {
|
||||
window.innerWidth = 800;
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
|
||||
// SidebarProvider should be rendered with open=false for small screens
|
||||
const sidebarProvider = screen.getByTestId("sidebar-provider");
|
||||
expect(sidebarProvider).toHaveAttribute("data-open", "false");
|
||||
});
|
||||
|
||||
test("updates screen size on window resize", async () => {
|
||||
window.innerWidth = 1200;
|
||||
const { rerender } = render(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
|
||||
// Initially large screen
|
||||
let sidebarProvider = screen.getByTestId("sidebar-provider");
|
||||
expect(sidebarProvider).toHaveAttribute("data-open", "true");
|
||||
|
||||
// Simulate window resize to small screen
|
||||
window.innerWidth = 800;
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
|
||||
// Force re-render to trigger useEffect
|
||||
rerender(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
|
||||
// Should now be small screen
|
||||
sidebarProvider = screen.getByTestId("sidebar-provider");
|
||||
expect(sidebarProvider).toHaveAttribute("data-open", "false");
|
||||
});
|
||||
|
||||
test("cleans up resize listener on unmount", () => {
|
||||
const removeEventListenerSpy = vi.spyOn(window, "removeEventListener");
|
||||
const { unmount } = render(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith("resize", expect.any(Function));
|
||||
// Check that all tabs are rendered in the sidebar
|
||||
mockTabs.forEach((tab) => {
|
||||
expect(screen.getByLabelText(tab.label)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TabContainer Integration", () => {
|
||||
test("renders active tab with correct title and description", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.EMAIL} />);
|
||||
test("renders responsive buttons for all tabs", () => {
|
||||
render(<ShareView {...defaultProps} />);
|
||||
|
||||
const tabContainer = screen.getByTestId("tab-container");
|
||||
expect(tabContainer).toBeInTheDocument();
|
||||
// Check that responsive buttons are rendered for all tabs
|
||||
const expectedTestIds = [
|
||||
"email-tab-icon",
|
||||
"website-embed-tab-icon",
|
||||
"dynamic-popup-tab-icon",
|
||||
"anonymous-links-tab-icon",
|
||||
"qr-code-tab-icon",
|
||||
"personal-links-tab-icon",
|
||||
"social-media-tab-icon",
|
||||
];
|
||||
|
||||
const tabTitle = screen.getByTestId("tab-title");
|
||||
expect(tabTitle).toHaveTextContent("Email");
|
||||
|
||||
const tabDescription = screen.getByTestId("tab-description");
|
||||
expect(tabDescription).toHaveTextContent("Email Description");
|
||||
|
||||
const tabContent = screen.getByTestId("email-tab-content");
|
||||
expect(tabContent).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders different tab when activeId changes", () => {
|
||||
const { rerender } = render(<ShareView {...defaultProps} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
// Initially shows Email tab
|
||||
expect(screen.getByTestId("tab-title")).toHaveTextContent("Email");
|
||||
expect(screen.getByTestId("email-tab-content")).toBeInTheDocument();
|
||||
|
||||
// Change to Website Embed tab
|
||||
rerender(<ShareView {...defaultProps} activeId={ShareViewType.WEBSITE_EMBED} />);
|
||||
|
||||
expect(screen.getByTestId("tab-title")).toHaveTextContent("Website Embed");
|
||||
expect(screen.getByTestId("website-embed-tab-content")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("email-tab-content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles tab without description", () => {
|
||||
const tabsWithoutDescription = [
|
||||
{
|
||||
id: ShareViewType.EMAIL,
|
||||
label: "Email",
|
||||
icon: () => <div data-testid="email-tab-icon" />,
|
||||
componentType: () => <div data-testid="email-tab-content">Email Content</div>,
|
||||
componentProps: {},
|
||||
title: "Email",
|
||||
// No description property
|
||||
},
|
||||
];
|
||||
|
||||
render(<ShareView {...defaultProps} tabs={tabsWithoutDescription} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
const tabDescription = screen.getByTestId("tab-description");
|
||||
expect(tabDescription).toHaveTextContent("");
|
||||
});
|
||||
|
||||
test("returns null when no active tab is found", () => {
|
||||
const emptyTabs: typeof mockTabs = [];
|
||||
|
||||
render(<ShareView {...defaultProps} tabs={emptyTabs} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
const tabContainer = screen.queryByTestId("tab-container");
|
||||
expect(tabContainer).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("SidebarProvider Configuration", () => {
|
||||
test("renders SidebarProvider with correct props for link surveys", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
|
||||
const sidebarProvider = screen.getByTestId("sidebar-provider");
|
||||
expect(sidebarProvider).toBeInTheDocument();
|
||||
expect(sidebarProvider).toHaveAttribute("data-open", "true");
|
||||
expect(sidebarProvider).toHaveClass("flex min-h-0 w-auto lg:col-span-1");
|
||||
expect(sidebarProvider).toHaveStyle("--sidebar-width: 100%");
|
||||
});
|
||||
|
||||
test("does not render SidebarProvider for non-link surveys", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyWeb} />);
|
||||
|
||||
expect(screen.queryByTestId("sidebar-provider")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correct grid layout for link surveys", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
|
||||
const container = screen.getByTestId("sidebar-provider").parentElement;
|
||||
expect(container).toHaveClass("lg:grid lg:grid-cols-4");
|
||||
});
|
||||
|
||||
test("does not render grid layout for non-link surveys", () => {
|
||||
const { container } = render(<ShareView {...defaultProps} survey={mockSurveyWeb} />);
|
||||
|
||||
const mainDiv = container.querySelector(".h-full > div");
|
||||
expect(mainDiv).not.toHaveClass("lg:grid lg:grid-cols-4");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sidebar Menu Buttons", () => {
|
||||
test("renders SidebarMenuButton with correct isActive prop", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
const emailButton = screen.getByLabelText("Email");
|
||||
expect(emailButton).toHaveAttribute("data-active", "true");
|
||||
|
||||
const websiteEmbedButton = screen.getByLabelText("Website Embed");
|
||||
expect(websiteEmbedButton).toHaveAttribute("data-active", "false");
|
||||
});
|
||||
|
||||
test("renders all tabs in sidebar menu", () => {
|
||||
render(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
|
||||
mockTabs.forEach((tab) => {
|
||||
const button = screen.getByLabelText(tab.label);
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAttribute("data-active", tab.id === ShareViewType.EMAIL ? "true" : "false");
|
||||
expectedTestIds.forEach((testId) => {
|
||||
const responsiveButtons = screen.getAllByTestId(testId);
|
||||
const responsiveButton = responsiveButtons.find((icon) => {
|
||||
const button = icon.closest("button");
|
||||
return button && button.getAttribute("data-variant") === "ghost";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Mobile Responsive Buttons", () => {
|
||||
test("renders mobile buttons for all tabs", () => {
|
||||
render(<ShareView {...defaultProps} />);
|
||||
|
||||
// Mobile buttons should be present for all tabs
|
||||
mockTabs.forEach((tab) => {
|
||||
// Map ShareViewType to actual testid used in the component
|
||||
const testIdMap: Record<string, string> = {
|
||||
[ShareViewType.ANON_LINKS]: "link-tab-icon",
|
||||
[ShareViewType.PERSONAL_LINKS]: "personal-links-tab-icon",
|
||||
[ShareViewType.WEBSITE_EMBED]: "website-embed-tab-icon",
|
||||
[ShareViewType.EMAIL]: "email-tab-icon",
|
||||
[ShareViewType.SOCIAL_MEDIA]: "social-media-tab-icon",
|
||||
[ShareViewType.QR_CODE]: "qr-code-tab-icon",
|
||||
[ShareViewType.DYNAMIC_POPUP]: "dynamic-popup-tab-icon",
|
||||
[ShareViewType.APP]: "app-tab-icon",
|
||||
};
|
||||
|
||||
const expectedTestId = testIdMap[tab.id] || `${tab.id}-tab-icon`;
|
||||
const mobileButtons = screen.getAllByTestId(expectedTestId);
|
||||
const mobileButton = mobileButtons.find((icon) => {
|
||||
const button = icon.closest("button");
|
||||
return button && button.getAttribute("data-variant") === "ghost";
|
||||
});
|
||||
expect(mobileButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("applies correct classes to mobile buttons based on active state", () => {
|
||||
render(<ShareView {...defaultProps} activeId={ShareViewType.WEBSITE_EMBED} />);
|
||||
|
||||
const websiteEmbedIcons = screen.getAllByTestId("website-embed-tab-icon");
|
||||
const activeMobileButton = websiteEmbedIcons
|
||||
.find((icon) => {
|
||||
const button = icon.closest("button");
|
||||
return button && button.getAttribute("data-variant") === "ghost";
|
||||
})
|
||||
?.closest("button");
|
||||
|
||||
if (activeMobileButton) {
|
||||
expect(activeMobileButton).toHaveClass("bg-white text-slate-900 shadow-sm hover:bg-white");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Content Area Layout", () => {
|
||||
test("applies correct column span for link surveys", () => {
|
||||
const { container } = render(<ShareView {...defaultProps} survey={mockSurveyLink} />);
|
||||
|
||||
const contentArea = container.querySelector('[class*="lg:col-span-3"]');
|
||||
expect(contentArea).toBeInTheDocument();
|
||||
expect(contentArea).toHaveClass("lg:col-span-3");
|
||||
});
|
||||
|
||||
test("does not apply column span for non-link surveys", () => {
|
||||
const { container } = render(<ShareView {...defaultProps} survey={mockSurveyWeb} />);
|
||||
|
||||
const contentArea = container.querySelector('[class*="lg:col-span-3"]');
|
||||
expect(contentArea).toBeNull();
|
||||
});
|
||||
|
||||
test("renders mobile button container with correct visibility class", () => {
|
||||
const { container } = render(<ShareView {...defaultProps} />);
|
||||
|
||||
const mobileButtonContainer = container.querySelector(".md\\:hidden");
|
||||
expect(mobileButtonContainer).toBeInTheDocument();
|
||||
expect(mobileButtonContainer).toHaveClass("md:hidden");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enhanced Tab Structure", () => {
|
||||
test("handles tabs with all required properties", () => {
|
||||
const completeTab = {
|
||||
id: ShareViewType.EMAIL,
|
||||
label: "Test Email",
|
||||
icon: () => <div data-testid="test-icon" />,
|
||||
componentType: () => <div data-testid="test-content">Test Content</div>,
|
||||
componentProps: {},
|
||||
title: "Test Title",
|
||||
description: "Test Description",
|
||||
};
|
||||
|
||||
render(<ShareView {...defaultProps} tabs={[completeTab]} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
expect(screen.getByTestId("tab-title")).toHaveTextContent("Test Title");
|
||||
expect(screen.getByTestId("tab-description")).toHaveTextContent("Test Description");
|
||||
expect(screen.getByTestId("test-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("uses title from tab definition in TabContainer", () => {
|
||||
const customTitleTab = {
|
||||
...mockTabs[0],
|
||||
title: "Custom Email Title",
|
||||
};
|
||||
|
||||
render(<ShareView {...defaultProps} tabs={[customTitleTab]} activeId={ShareViewType.EMAIL} />);
|
||||
|
||||
expect(screen.getByTestId("tab-title")).toHaveTextContent("Custom Email Title");
|
||||
expect(responsiveButton).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,6 @@ 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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface ShareViewProps {
|
||||
tabs: Array<{
|
||||
@@ -33,10 +32,9 @@ interface ShareViewProps {
|
||||
}>;
|
||||
activeId: ShareViewType;
|
||||
setActiveId: React.Dispatch<React.SetStateAction<ShareViewType>>;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const ShareView = ({ tabs, activeId, setActiveId, survey }: ShareViewProps) => {
|
||||
export const ShareView = ({ tabs, activeId, setActiveId }: ShareViewProps) => {
|
||||
const { t } = useTranslate();
|
||||
const [isLargeScreen, setIsLargeScreen] = useState(true);
|
||||
|
||||
@@ -67,52 +65,48 @@ export const ShareView = ({ tabs, activeId, setActiveId, survey }: ShareViewProp
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className={`flex h-full ${survey.type === "link" ? "lg:grid lg:grid-cols-4" : ""}`}>
|
||||
{survey.type === "link" && (
|
||||
<SidebarProvider
|
||||
open={isLargeScreen}
|
||||
className="flex min-h-0 w-auto lg:col-span-1"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "100%",
|
||||
} as React.CSSProperties
|
||||
}>
|
||||
<Sidebar className="relative h-full p-0" variant="inset" collapsible="icon">
|
||||
<SidebarContent className="h-full border-r border-slate-200 bg-white p-4">
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarGroupLabel>
|
||||
<Small className="text-xs text-slate-500">
|
||||
{t("environments.surveys.share.share_view_title")}
|
||||
</Small>
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="flex flex-col gap-1">
|
||||
{tabs.map((tab) => (
|
||||
<SidebarMenuItem key={tab.id}>
|
||||
<SidebarMenuButton
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
"flex w-full justify-start rounded-md p-2 text-slate-600 hover:bg-slate-100 hover:text-slate-900",
|
||||
tab.id === activeId
|
||||
? "bg-slate-100 font-medium text-slate-900"
|
||||
: "text-slate-700"
|
||||
)}
|
||||
tooltip={tab.label}
|
||||
isActive={tab.id === activeId}>
|
||||
<tab.icon className="h-4 w-4 text-slate-700" />
|
||||
<span>{tab.label}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
)}
|
||||
<div className={`flex h-full lg:grid lg:grid-cols-4`}>
|
||||
<SidebarProvider
|
||||
open={isLargeScreen}
|
||||
className="flex min-h-0 w-auto lg:col-span-1"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "100%",
|
||||
} as React.CSSProperties
|
||||
}>
|
||||
<Sidebar className="relative h-full p-0" variant="inset" collapsible="icon">
|
||||
<SidebarContent className="h-full rounded-l-lg border-r border-slate-200 bg-white p-4">
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarGroupLabel>
|
||||
<Small className="text-xs text-slate-500">
|
||||
{t("environments.surveys.share.share_view_title")}
|
||||
</Small>
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="flex flex-col gap-1">
|
||||
{tabs.map((tab) => (
|
||||
<SidebarMenuItem key={tab.id}>
|
||||
<SidebarMenuButton
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
"flex w-full justify-start rounded-md p-2 text-slate-600 hover:bg-slate-100 hover:text-slate-900",
|
||||
tab.id === activeId ? "bg-slate-100 font-medium text-slate-900" : "text-slate-700"
|
||||
)}
|
||||
tooltip={tab.label}
|
||||
isActive={tab.id === activeId}>
|
||||
<tab.icon className="h-4 w-4 text-slate-700" />
|
||||
<span>{tab.label}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
<div
|
||||
className={`h-full w-full grow overflow-y-auto bg-slate-50 px-4 py-6 lg:p-6 ${survey.type === "link" ? "lg:col-span-3" : ""}`}>
|
||||
className={`h-full w-full grow overflow-y-auto rounded-lg bg-slate-50 px-4 py-6 md:rounded-l-lg lg:col-span-3 lg:p-6`}>
|
||||
{renderActiveTab()}
|
||||
<div className="flex justify-center gap-2 rounded-md pt-6 text-center md:hidden">
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -50,13 +50,6 @@ describe("TabContainer", () => {
|
||||
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} />);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const TabContainer = ({ title, description, children }: TabContainerProps
|
||||
{description}
|
||||
</Small>
|
||||
</div>
|
||||
{children}
|
||||
<div className="h-full w-full space-y-4 overflow-y-auto">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -180,13 +180,4 @@ describe("WebsiteEmbedTab", () => {
|
||||
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.share.embed_on_website.embed_mode");
|
||||
expect(toggle).toHaveTextContent("environments.surveys.share.embed_on_website.embed_mode_description");
|
||||
expect(screen.getByTestId("custom-container-class")).toHaveTextContent("p-0");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,9 +35,10 @@ export const WebsiteEmbedTab = ({ surveyUrl }: WebsiteEmbedTabProps) => {
|
||||
onToggle={setEmbedModeEnabled}
|
||||
title={t("environments.surveys.share.embed_on_website.embed_mode")}
|
||||
description={t("environments.surveys.share.embed_on_website.embed_mode_description")}
|
||||
customContainerClass="p-0"
|
||||
customContainerClass="pl-1 pr-0 py-0"
|
||||
/>
|
||||
<Button
|
||||
className="self-start"
|
||||
title={t("common.copy_code")}
|
||||
aria-label={t("common.copy_code")}
|
||||
onClick={() => {
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { SurveyContextWrapper, useSurvey } from "./survey-context";
|
||||
|
||||
// Mock survey data
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "test-survey-id",
|
||||
createdAt: new Date("2023-01-01T00:00:00.000Z"),
|
||||
updatedAt: new Date("2023-01-01T00:00:00.000Z"),
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
environmentId: "test-env-id",
|
||||
createdBy: "test-user-id",
|
||||
status: "draft",
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "Welcome" },
|
||||
html: { default: "" },
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
buttonLabel: { default: "Start" },
|
||||
fileUrl: undefined,
|
||||
videoUrl: undefined,
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
id: "question-1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "What's your name?" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
logic: [],
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
placeholder: { default: "Enter your name" },
|
||||
longAnswer: false,
|
||||
subheader: { default: "" },
|
||||
charLimit: { enabled: false, min: 0, max: 255 },
|
||||
},
|
||||
],
|
||||
endings: [
|
||||
{
|
||||
id: "ending-1",
|
||||
type: "endScreen",
|
||||
headline: { default: "Thank you!" },
|
||||
subheader: { default: "We appreciate your feedback." },
|
||||
buttonLabel: { default: "Done" },
|
||||
buttonLink: undefined,
|
||||
imageUrl: undefined,
|
||||
videoUrl: undefined,
|
||||
},
|
||||
],
|
||||
hiddenFields: {
|
||||
enabled: false,
|
||||
fieldIds: [],
|
||||
},
|
||||
variables: [],
|
||||
followUps: [],
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
projectOverwrites: null,
|
||||
styling: null,
|
||||
showLanguageSwitch: null,
|
||||
surveyClosedMessage: null,
|
||||
segment: null,
|
||||
singleUse: null,
|
||||
isVerifyEmailEnabled: false,
|
||||
recaptcha: null,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
isBackButtonHidden: false,
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
displayPercentage: null,
|
||||
languages: [
|
||||
{
|
||||
language: {
|
||||
id: "en",
|
||||
code: "en",
|
||||
alias: "English",
|
||||
projectId: "test-project-id",
|
||||
createdAt: new Date("2023-01-01T00:00:00.000Z"),
|
||||
updatedAt: new Date("2023-01-01T00:00:00.000Z"),
|
||||
},
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Test component that uses the hook
|
||||
const TestComponent = () => {
|
||||
const { survey } = useSurvey();
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="survey-id">{survey.id}</div>
|
||||
<div data-testid="survey-name">{survey.name}</div>
|
||||
<div data-testid="survey-type">{survey.type}</div>
|
||||
<div data-testid="survey-status">{survey.status}</div>
|
||||
<div data-testid="survey-environment-id">{survey.environmentId}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe("SurveyContext", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("provides survey data to child components", () => {
|
||||
render(
|
||||
<SurveyContextWrapper survey={mockSurvey}>
|
||||
<TestComponent />
|
||||
</SurveyContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("survey-id")).toHaveTextContent("test-survey-id");
|
||||
expect(screen.getByTestId("survey-name")).toHaveTextContent("Test Survey");
|
||||
expect(screen.getByTestId("survey-type")).toHaveTextContent("link");
|
||||
expect(screen.getByTestId("survey-status")).toHaveTextContent("draft");
|
||||
expect(screen.getByTestId("survey-environment-id")).toHaveTextContent("test-env-id");
|
||||
});
|
||||
|
||||
test("throws error when useSurvey is used outside of provider", () => {
|
||||
const TestComponentWithoutProvider = () => {
|
||||
useSurvey();
|
||||
return <div>Should not render</div>;
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<TestComponentWithoutProvider />);
|
||||
}).toThrow("useSurvey must be used within a SurveyContextWrapper");
|
||||
});
|
||||
|
||||
test("updates context value when survey changes", () => {
|
||||
const updatedSurvey = {
|
||||
...mockSurvey,
|
||||
name: "Updated Survey",
|
||||
status: "inProgress" as const,
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<SurveyContextWrapper survey={mockSurvey}>
|
||||
<TestComponent />
|
||||
</SurveyContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("survey-name")).toHaveTextContent("Test Survey");
|
||||
expect(screen.getByTestId("survey-status")).toHaveTextContent("draft");
|
||||
|
||||
rerender(
|
||||
<SurveyContextWrapper survey={updatedSurvey}>
|
||||
<TestComponent />
|
||||
</SurveyContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("survey-name")).toHaveTextContent("Updated Survey");
|
||||
expect(screen.getByTestId("survey-status")).toHaveTextContent("inProgress");
|
||||
});
|
||||
|
||||
test("verifies memoization by tracking render counts", () => {
|
||||
let renderCount = 0;
|
||||
const renderSpy = vi.fn(() => {
|
||||
renderCount++;
|
||||
});
|
||||
|
||||
const TestComponentWithRenderTracking = () => {
|
||||
renderSpy();
|
||||
const { survey } = useSurvey();
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="survey-id">{survey.id}</div>
|
||||
<div data-testid="render-count">{renderCount}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<SurveyContextWrapper survey={mockSurvey}>
|
||||
<TestComponentWithRenderTracking />
|
||||
</SurveyContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("survey-id")).toHaveTextContent("test-survey-id");
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Rerender with the same survey object - should not trigger additional renders
|
||||
// if memoization is working correctly
|
||||
rerender(
|
||||
<SurveyContextWrapper survey={mockSurvey}>
|
||||
<TestComponentWithRenderTracking />
|
||||
</SurveyContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("survey-id")).toHaveTextContent("test-survey-id");
|
||||
expect(renderSpy).toHaveBeenCalledTimes(2); // Should only be called once more for the rerender
|
||||
});
|
||||
|
||||
test("prevents unnecessary re-renders when survey object is unchanged", () => {
|
||||
const childRenderSpy = vi.fn();
|
||||
|
||||
const ChildComponent = () => {
|
||||
childRenderSpy();
|
||||
const { survey } = useSurvey();
|
||||
return <div data-testid="child-survey-name">{survey.name}</div>;
|
||||
};
|
||||
|
||||
const ParentComponent = ({ survey }: { survey: TSurvey }) => {
|
||||
return (
|
||||
<SurveyContextWrapper survey={survey}>
|
||||
<ChildComponent />
|
||||
</SurveyContextWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const { rerender } = render(<ParentComponent survey={mockSurvey} />);
|
||||
|
||||
expect(screen.getByTestId("child-survey-name")).toHaveTextContent("Test Survey");
|
||||
expect(childRenderSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Rerender with the same survey object reference
|
||||
rerender(<ParentComponent survey={mockSurvey} />);
|
||||
|
||||
expect(screen.getByTestId("child-survey-name")).toHaveTextContent("Test Survey");
|
||||
expect(childRenderSpy).toHaveBeenCalledTimes(2); // Should only be called once more
|
||||
|
||||
// Rerender with a different survey object should trigger re-render
|
||||
const updatedSurvey = { ...mockSurvey, name: "Updated Survey" };
|
||||
rerender(<ParentComponent survey={updatedSurvey} />);
|
||||
|
||||
expect(screen.getByTestId("child-survey-name")).toHaveTextContent("Updated Survey");
|
||||
expect(childRenderSpy).toHaveBeenCalledTimes(3); // Should be called again due to prop change
|
||||
});
|
||||
|
||||
test("renders children correctly", () => {
|
||||
const TestChild = () => <div data-testid="child">Child Component</div>;
|
||||
|
||||
render(
|
||||
<SurveyContextWrapper survey={mockSurvey}>
|
||||
<TestChild />
|
||||
</SurveyContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("child")).toHaveTextContent("Child Component");
|
||||
});
|
||||
|
||||
test("handles multiple child components", () => {
|
||||
const TestChild1 = () => {
|
||||
const { survey } = useSurvey();
|
||||
return <div data-testid="child-1">{survey.name}</div>;
|
||||
};
|
||||
|
||||
const TestChild2 = () => {
|
||||
const { survey } = useSurvey();
|
||||
return <div data-testid="child-2">{survey.type}</div>;
|
||||
};
|
||||
|
||||
render(
|
||||
<SurveyContextWrapper survey={mockSurvey}>
|
||||
<TestChild1 />
|
||||
<TestChild2 />
|
||||
</SurveyContextWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("child-1")).toHaveTextContent("Test Survey");
|
||||
expect(screen.getByTestId("child-2")).toHaveTextContent("link");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export interface SurveyContextType {
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
const SurveyContext = createContext<SurveyContextType | null>(null);
|
||||
SurveyContext.displayName = "SurveyContext";
|
||||
|
||||
export const useSurvey = () => {
|
||||
const context = useContext(SurveyContext);
|
||||
if (!context) {
|
||||
throw new Error("useSurvey must be used within a SurveyContextWrapper");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Client wrapper component to be used in server components
|
||||
interface SurveyContextWrapperProps {
|
||||
survey: TSurvey;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SurveyContextWrapper = ({ survey, children }: SurveyContextWrapperProps) => {
|
||||
const surveyContextValue = useMemo(
|
||||
() => ({
|
||||
survey,
|
||||
}),
|
||||
[survey]
|
||||
);
|
||||
|
||||
return <SurveyContext.Provider value={surveyContextValue}>{children}</SurveyContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
// Import the mocked function to access it in tests
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import SurveyLayout from "./layout";
|
||||
|
||||
// Mock the getSurvey function
|
||||
vi.mock("@/lib/survey/service", () => ({
|
||||
getSurvey: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the SurveyContextWrapper component
|
||||
vi.mock("./context/survey-context", () => ({
|
||||
SurveyContextWrapper: ({ survey, children }: { survey: TSurvey; children: React.ReactNode }) => (
|
||||
<div data-testid="survey-context-wrapper" data-survey-id={survey.id}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("SurveyLayout", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "survey-123",
|
||||
name: "Test Survey",
|
||||
environmentId: "env-123",
|
||||
status: "inProgress",
|
||||
type: "link",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
questions: [],
|
||||
endings: [],
|
||||
hiddenFields: {
|
||||
enabled: false,
|
||||
},
|
||||
variables: [],
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
timeToFinish: true,
|
||||
showResponseCount: false,
|
||||
},
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
autoClose: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
isVerifyEmailEnabled: false,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
isBackButtonHidden: false,
|
||||
projectOverwrites: {
|
||||
brandColor: null,
|
||||
highlightBorderColor: null,
|
||||
placement: null,
|
||||
clickOutsideClose: null,
|
||||
darkOverlay: null,
|
||||
},
|
||||
styling: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUse: null,
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
showLanguageSwitch: false,
|
||||
recaptcha: null,
|
||||
languages: [],
|
||||
triggers: [],
|
||||
segment: null,
|
||||
followUps: [],
|
||||
createdBy: null,
|
||||
};
|
||||
|
||||
const mockParams = Promise.resolve({
|
||||
surveyId: "survey-123",
|
||||
environmentId: "env-123",
|
||||
});
|
||||
|
||||
test("renders SurveyContextWrapper with survey and children when survey is found", async () => {
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
|
||||
render(
|
||||
await SurveyLayout({
|
||||
params: mockParams,
|
||||
children: <div data-testid="test-children">Test Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(getSurvey).toHaveBeenCalledWith("survey-123");
|
||||
expect(screen.getByTestId("survey-context-wrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("survey-context-wrapper")).toHaveAttribute("data-survey-id", "survey-123");
|
||||
expect(screen.getByTestId("test-children")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("throws error when survey is not found", async () => {
|
||||
vi.mocked(getSurvey).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
SurveyLayout({
|
||||
params: mockParams,
|
||||
children: <div data-testid="test-children">Test Content</div>,
|
||||
})
|
||||
).rejects.toThrow("Survey not found");
|
||||
|
||||
expect(getSurvey).toHaveBeenCalledWith("survey-123");
|
||||
});
|
||||
|
||||
test("awaits params before calling getSurvey", async () => {
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
|
||||
const delayedParams = new Promise<{ surveyId: string; environmentId: string }>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
surveyId: "survey-456",
|
||||
environmentId: "env-456",
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
|
||||
render(
|
||||
await SurveyLayout({
|
||||
params: delayedParams,
|
||||
children: <div data-testid="test-children">Test Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(getSurvey).toHaveBeenCalledWith("survey-456");
|
||||
expect(screen.getByTestId("survey-context-wrapper")).toHaveAttribute("data-survey-id", "survey-123");
|
||||
});
|
||||
|
||||
test("calls getSurvey with correct surveyId from params", async () => {
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
|
||||
const customParams = Promise.resolve({
|
||||
surveyId: "custom-survey-id",
|
||||
environmentId: "custom-env-id",
|
||||
});
|
||||
|
||||
render(
|
||||
await SurveyLayout({
|
||||
params: customParams,
|
||||
children: <div data-testid="test-children">Test Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(getSurvey).toHaveBeenCalledWith("custom-survey-id");
|
||||
expect(getSurvey).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("passes children to SurveyContextWrapper", async () => {
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
|
||||
const complexChildren = (
|
||||
<div data-testid="complex-children">
|
||||
<h1>Survey Title</h1>
|
||||
<p>Survey description</p>
|
||||
<button>Submit</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
await SurveyLayout({
|
||||
params: mockParams,
|
||||
children: complexChildren,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("complex-children")).toBeInTheDocument();
|
||||
expect(screen.getByText("Survey Title")).toBeInTheDocument();
|
||||
expect(screen.getByText("Survey description")).toBeInTheDocument();
|
||||
expect(screen.getByText("Submit")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles getSurvey rejection correctly", async () => {
|
||||
const mockError = new Error("Database connection failed");
|
||||
vi.mocked(getSurvey).mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
SurveyLayout({
|
||||
params: mockParams,
|
||||
children: <div data-testid="test-children">Test Content</div>,
|
||||
})
|
||||
).rejects.toThrow("Database connection failed");
|
||||
|
||||
expect(getSurvey).toHaveBeenCalledWith("survey-123");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { SurveyContextWrapper } from "./context/survey-context";
|
||||
|
||||
interface SurveyLayoutProps {
|
||||
params: Promise<{ surveyId: string; environmentId: string }>;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SurveyLayout = async ({ params, children }: SurveyLayoutProps) => {
|
||||
const resolvedParams = await params;
|
||||
|
||||
const survey = await getSurvey(resolvedParams.surveyId);
|
||||
|
||||
if (!survey) {
|
||||
throw new Error("Survey not found");
|
||||
}
|
||||
|
||||
return <SurveyContextWrapper survey={survey}>{children}</SurveyContextWrapper>;
|
||||
};
|
||||
|
||||
export default SurveyLayout;
|
||||
@@ -108,6 +108,36 @@ const baseSurvey: TSurvey = {
|
||||
recaptcha: { enabled: false, threshold: 0.5 },
|
||||
};
|
||||
|
||||
// Helper function to create mock display objects
|
||||
const createMockDisplay = (id: string, surveyId: string, contactId: string, createdAt?: Date) => ({
|
||||
id,
|
||||
createdAt: createdAt || new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId,
|
||||
contactId,
|
||||
responseId: null,
|
||||
status: null,
|
||||
});
|
||||
|
||||
// Helper function to create mock response objects
|
||||
const createMockResponse = (id: string, surveyId: string, contactId: string, createdAt?: Date) => ({
|
||||
id,
|
||||
createdAt: createdAt || new Date(),
|
||||
updatedAt: new Date(),
|
||||
finished: false,
|
||||
surveyId,
|
||||
contactId,
|
||||
endingId: null,
|
||||
data: {},
|
||||
variables: {},
|
||||
ttc: {},
|
||||
meta: {},
|
||||
contactAttributes: null,
|
||||
singleUseId: null,
|
||||
language: null,
|
||||
displayId: null,
|
||||
});
|
||||
|
||||
describe("getSyncSurveys", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValue(mockProject);
|
||||
@@ -125,7 +155,7 @@ describe("getSyncSurveys", () => {
|
||||
test("should throw error if product not found", async () => {
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValue(null);
|
||||
await expect(getSyncSurveys(environmentId, contactId, contactAttributes, deviceType)).rejects.toThrow(
|
||||
"Product not found"
|
||||
"Project not found"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -148,7 +178,7 @@ describe("getSyncSurveys", () => {
|
||||
test("should filter by displayOption 'displayOnce'", async () => {
|
||||
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", displayOption: "displayOnce" }];
|
||||
vi.mocked(getSurveys).mockResolvedValue(surveys);
|
||||
vi.mocked(prisma.display.findMany).mockResolvedValue([{ id: "d1", surveyId: "s1", contactId }]); // Already displayed
|
||||
vi.mocked(prisma.display.findMany).mockResolvedValue([createMockDisplay("d1", "s1", contactId)]); // Already displayed
|
||||
|
||||
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
|
||||
expect(result).toEqual([]);
|
||||
@@ -161,7 +191,7 @@ describe("getSyncSurveys", () => {
|
||||
test("should filter by displayOption 'displayMultiple'", async () => {
|
||||
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", displayOption: "displayMultiple" }];
|
||||
vi.mocked(getSurveys).mockResolvedValue(surveys);
|
||||
vi.mocked(prisma.response.findMany).mockResolvedValue([{ id: "r1", surveyId: "s1", contactId }]); // Already responded
|
||||
vi.mocked(prisma.response.findMany).mockResolvedValue([createMockResponse("r1", "s1", contactId)]); // Already responded
|
||||
|
||||
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
|
||||
expect(result).toEqual([]);
|
||||
@@ -175,19 +205,19 @@ describe("getSyncSurveys", () => {
|
||||
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", displayOption: "displaySome", displayLimit: 2 }];
|
||||
vi.mocked(getSurveys).mockResolvedValue(surveys);
|
||||
vi.mocked(prisma.display.findMany).mockResolvedValue([
|
||||
{ id: "d1", surveyId: "s1", contactId },
|
||||
{ id: "d2", surveyId: "s1", contactId },
|
||||
createMockDisplay("d1", "s1", contactId),
|
||||
createMockDisplay("d2", "s1", contactId),
|
||||
]); // Display limit reached
|
||||
|
||||
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
|
||||
expect(result).toEqual([]);
|
||||
|
||||
vi.mocked(prisma.display.findMany).mockResolvedValue([{ id: "d1", surveyId: "s1", contactId }]); // Within limit
|
||||
vi.mocked(prisma.display.findMany).mockResolvedValue([createMockDisplay("d1", "s1", contactId)]); // Within limit
|
||||
const result2 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
|
||||
expect(result2).toEqual(surveys);
|
||||
|
||||
// Test with response already submitted
|
||||
vi.mocked(prisma.response.findMany).mockResolvedValue([{ id: "r1", surveyId: "s1", contactId }]);
|
||||
vi.mocked(prisma.response.findMany).mockResolvedValue([createMockResponse("r1", "s1", contactId)]);
|
||||
const result3 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
|
||||
expect(result3).toEqual([]);
|
||||
});
|
||||
@@ -195,8 +225,8 @@ describe("getSyncSurveys", () => {
|
||||
test("should not filter by displayOption 'respondMultiple'", async () => {
|
||||
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", displayOption: "respondMultiple" }];
|
||||
vi.mocked(getSurveys).mockResolvedValue(surveys);
|
||||
vi.mocked(prisma.display.findMany).mockResolvedValue([{ id: "d1", surveyId: "s1", contactId }]);
|
||||
vi.mocked(prisma.response.findMany).mockResolvedValue([{ id: "r1", surveyId: "s1", contactId }]);
|
||||
vi.mocked(prisma.display.findMany).mockResolvedValue([createMockDisplay("d1", "s1", contactId)]);
|
||||
vi.mocked(prisma.response.findMany).mockResolvedValue([createMockResponse("r1", "s1", contactId)]);
|
||||
|
||||
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
|
||||
expect(result).toEqual(surveys);
|
||||
@@ -207,7 +237,7 @@ describe("getSyncSurveys", () => {
|
||||
vi.mocked(getSurveys).mockResolvedValue(surveys);
|
||||
const displayDate = new Date();
|
||||
vi.mocked(prisma.display.findMany).mockResolvedValue([
|
||||
{ id: "d1", surveyId: "s2", contactId, createdAt: displayDate }, // Display for another survey
|
||||
createMockDisplay("d1", "s2", contactId, displayDate), // Display for another survey
|
||||
]);
|
||||
|
||||
vi.mocked(diffInDays).mockReturnValue(5); // Not enough days passed (product.recontactDays = 10)
|
||||
|
||||
@@ -22,10 +22,10 @@ export const getSyncSurveys = reactCache(
|
||||
): Promise<TSurvey[]> => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
try {
|
||||
const product = await getProjectByEnvironmentId(environmentId);
|
||||
const project = await getProjectByEnvironmentId(environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
if (!project) {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
let surveys = await getSurveys(environmentId);
|
||||
@@ -89,8 +89,8 @@ export const getSyncSurveys = reactCache(
|
||||
return true;
|
||||
}
|
||||
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
|
||||
} else if (product.recontactDays !== null) {
|
||||
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
|
||||
} else if (project.recontactDays !== null) {
|
||||
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= project.recontactDays;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1803,6 +1803,35 @@
|
||||
"go_to_setup_checklist": "Gehe zur Einrichtungs-Checkliste \uD83D\uDC49",
|
||||
"impressions": "Eindrücke",
|
||||
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
|
||||
"in_app": {
|
||||
"connection_description": "Die Umfrage wird den Nutzern Ihrer Website angezeigt, die den unten aufgeführten Kriterien entsprechen",
|
||||
"connection_title": "Formbricks SDK ist verbunden",
|
||||
"description": "Formbricks Umfragen können als Pop-up eingebettet werden, basierend auf der Benutzerinteraktion.",
|
||||
"display_criteria": "Anzeigekriterien",
|
||||
"display_criteria.audience_description": "Zielgruppe",
|
||||
"display_criteria.code_trigger": "Code Aktion",
|
||||
"display_criteria.everyone": "Jeder",
|
||||
"display_criteria.no_code_trigger": "Kein Code",
|
||||
"display_criteria.overwritten": "Überschrieben",
|
||||
"display_criteria.randomizer": "{percentage}% Zufallsgenerator",
|
||||
"display_criteria.randomizer_description": "Nur {percentage}% der Personen, die die Aktion ausführen, könnten befragt werden.",
|
||||
"display_criteria.recontact_description": "Optionen zur erneuten Kontaktaufnahme",
|
||||
"display_criteria.targeted": "Gezielt",
|
||||
"display_criteria.time_based_always": "Umfrage immer anzeigen",
|
||||
"display_criteria.time_based_day": "Tag",
|
||||
"display_criteria.time_based_days": "Tage",
|
||||
"display_criteria.time_based_description": "Globale Wartezeit",
|
||||
"display_criteria.trigger_description": "Umfrageauslöser",
|
||||
"documentation_title": "Unterbrechungsumfragen auf allen Plattformen verteilen",
|
||||
"html_embed": "HTML-Einbettung im <head>",
|
||||
"ios_sdk": "iOS SDK für Apple-Apps",
|
||||
"javascript_sdk": "JavaScript SDK",
|
||||
"kotlin_sdk": "Kotlin SDK für Android-Apps",
|
||||
"no_connection_description": "Verbinde deine Website oder App mit Formbricks, um Abfangumfragen zu veröffentlichen.",
|
||||
"no_connection_title": "Du bist noch nicht verbunden!",
|
||||
"react_native_sdk": "React Native SDK für RN-Apps.",
|
||||
"title": "Feedback-Befragungseinstellungen"
|
||||
},
|
||||
"includes_all": "Beinhaltet alles",
|
||||
"includes_either": "Beinhaltet entweder",
|
||||
"install_widget": "Formbricks Widget installieren",
|
||||
|
||||
@@ -1803,6 +1803,35 @@
|
||||
"go_to_setup_checklist": "Go to Setup Checklist \uD83D\uDC49",
|
||||
"impressions": "Impressions",
|
||||
"impressions_tooltip": "Number of times the survey has been viewed.",
|
||||
"in_app": {
|
||||
"connection_description": "The survey will be shown to users of your website, that match the criteria listed below",
|
||||
"connection_title": "Formbricks SDK is connected",
|
||||
"description": "Formbricks surveys can be embedded as a pop-up, based on user interaction.",
|
||||
"display_criteria": "Display criteria",
|
||||
"display_criteria.audience_description": "Target audience",
|
||||
"display_criteria.code_trigger": "Code Action",
|
||||
"display_criteria.everyone": "Everyone",
|
||||
"display_criteria.no_code_trigger": "No-Code",
|
||||
"display_criteria.overwritten": "Overwritten",
|
||||
"display_criteria.randomizer": "{percentage}% Randomizer",
|
||||
"display_criteria.randomizer_description": "Only {percentage}% of people who perform the action might get surveyed.",
|
||||
"display_criteria.recontact_description": "Recontact options",
|
||||
"display_criteria.targeted": "Targeted",
|
||||
"display_criteria.time_based_always": "Always show survey",
|
||||
"display_criteria.time_based_day": "Day",
|
||||
"display_criteria.time_based_days": "Days",
|
||||
"display_criteria.time_based_description": "Global waiting time",
|
||||
"display_criteria.trigger_description": "Survey trigger",
|
||||
"documentation_title": "Distribute intercept surveys on all platforms",
|
||||
"html_embed": "HTML embed in <head>",
|
||||
"ios_sdk": "iOS SDK for Apple apps",
|
||||
"javascript_sdk": "JavaScript SDK",
|
||||
"kotlin_sdk": "Kotlin SDK for Android apps",
|
||||
"no_connection_description": "Connect your website or app with Formbricks to publish intercept surveys.",
|
||||
"no_connection_title": "You're not plugged in yet!",
|
||||
"react_native_sdk": "React Native SDK for RN apps.",
|
||||
"title": "Intercept survey settings"
|
||||
},
|
||||
"includes_all": "Includes all",
|
||||
"includes_either": "Includes either",
|
||||
"install_widget": "Install Formbricks Widget",
|
||||
|
||||
@@ -1803,6 +1803,35 @@
|
||||
"go_to_setup_checklist": "Allez à la liste de contrôle de configuration \uD83D\uDC49",
|
||||
"impressions": "Impressions",
|
||||
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
|
||||
"in_app": {
|
||||
"connection_description": "Le sondage sera affiché aux utilisateurs de votre site web, qui correspondent aux critères listés ci-dessous",
|
||||
"connection_title": "Le SDK Formbricks est connecté",
|
||||
"description": "Les enquêtes Formbricks peuvent être intégrées sous forme de pop-up, en fonction de l'interaction de l'utilisateur.",
|
||||
"display_criteria": "Critères d'affichage",
|
||||
"display_criteria.audience_description": "Public cible",
|
||||
"display_criteria.code_trigger": "Code Action",
|
||||
"display_criteria.everyone": "Tout le monde",
|
||||
"display_criteria.no_code_trigger": "Pas de code",
|
||||
"display_criteria.overwritten": "Réécrit",
|
||||
"display_criteria.randomizer": "{percentage}% Randomiseur",
|
||||
"display_criteria.randomizer_description": "Seulement {percentage}% des personnes qui réalisent l'action pourraient être sondées.",
|
||||
"display_criteria.recontact_description": "Options de recontact",
|
||||
"display_criteria.targeted": "Ciblé",
|
||||
"display_criteria.time_based_always": "Afficher toujours l'enquête",
|
||||
"display_criteria.time_based_day": "Jour",
|
||||
"display_criteria.time_based_days": "Jours",
|
||||
"display_criteria.time_based_description": "Temps d'attente global",
|
||||
"display_criteria.trigger_description": "Déclencheur d'enquête",
|
||||
"documentation_title": "Distribuer des sondages d'interception sur toutes les plateformes",
|
||||
"html_embed": "Code HTML intégré dans <head>",
|
||||
"ios_sdk": "SDK iOS pour les applications Apple",
|
||||
"javascript_sdk": "SDK JavaScript",
|
||||
"kotlin_sdk": "Kotlin SDK pour applications Android",
|
||||
"no_connection_description": "Connectez votre site web ou votre application à Formbricks pour publier des sondages interceptés.",
|
||||
"no_connection_title": "Vous n'êtes pas encore branché !",
|
||||
"react_native_sdk": "SDK React Native pour les applications RN",
|
||||
"title": "Paramètres de sondage par interception"
|
||||
},
|
||||
"includes_all": "Comprend tous",
|
||||
"includes_either": "Comprend soit",
|
||||
"install_widget": "Installer le widget Formbricks",
|
||||
|
||||
@@ -1803,6 +1803,35 @@
|
||||
"go_to_setup_checklist": "Vai para a Lista de Configuração \uD83D\uDC49",
|
||||
"impressions": "Impressões",
|
||||
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
|
||||
"in_app": {
|
||||
"connection_description": "A pesquisa será exibida para usuários do seu site, que atendam aos critérios listados abaixo",
|
||||
"connection_title": "O SDK do Formbricks está conectado",
|
||||
"description": "\"As pesquisas do Formbricks podem ser embutidas como um pop-up, de acordo com a interação do usuário.\"",
|
||||
"display_criteria": "Exibir critérios",
|
||||
"display_criteria.audience_description": "Público-alvo",
|
||||
"display_criteria.code_trigger": "Ação de Código",
|
||||
"display_criteria.everyone": "Todo mundo",
|
||||
"display_criteria.no_code_trigger": "Sem código",
|
||||
"display_criteria.overwritten": "Sobrescrito",
|
||||
"display_criteria.randomizer": "Randomizador {percentage}%",
|
||||
"display_criteria.randomizer_description": "Apenas {percentage}% das pessoas que realizam a ação podem ser pesquisadas.",
|
||||
"display_criteria.recontact_description": "Opções de Recontato",
|
||||
"display_criteria.targeted": "direcionado",
|
||||
"display_criteria.time_based_always": "Mostrar pesquisa sempre",
|
||||
"display_criteria.time_based_day": "Dia",
|
||||
"display_criteria.time_based_days": "Dias",
|
||||
"display_criteria.time_based_description": "Tempo de espera global",
|
||||
"display_criteria.trigger_description": "Gatilho de Pesquisa",
|
||||
"documentation_title": "Distribua pesquisas de interceptação em todas as plataformas",
|
||||
"html_embed": "HTML embutido no <head>",
|
||||
"ios_sdk": "SDK iOS para aplicativos da Apple",
|
||||
"javascript_sdk": "SDK JavaScript",
|
||||
"kotlin_sdk": "SDK Kotlin para aplicativos Android",
|
||||
"no_connection_description": "Conecte seu site ou app com o Formbricks para publicar pesquisas de interceptação.",
|
||||
"no_connection_title": "Você ainda não tá conectado!",
|
||||
"react_native_sdk": "SDK React Native para apps RN",
|
||||
"title": "Configurações de interceptação de pesquisa"
|
||||
},
|
||||
"includes_all": "Inclui tudo",
|
||||
"includes_either": "Inclui ou",
|
||||
"install_widget": "Instalar Widget do Formbricks",
|
||||
|
||||
@@ -1803,6 +1803,35 @@
|
||||
"go_to_setup_checklist": "Ir para a Lista de Verificação de Configuração \uD83D\uDC49",
|
||||
"impressions": "Impressões",
|
||||
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
|
||||
"in_app": {
|
||||
"connection_description": "O questionário será exibido aos utilizadores do seu website que correspondam aos critérios listados abaixo",
|
||||
"connection_title": "O SDK do Formbricks está conectado",
|
||||
"description": "Os inquéritos Formbricks podem ser incorporados como uma janela pop-up, com base na interação do utilizador.",
|
||||
"display_criteria": "Critérios de exibição",
|
||||
"display_criteria.audience_description": "Público-alvo",
|
||||
"display_criteria.code_trigger": "Código de Ação",
|
||||
"display_criteria.everyone": "Todos",
|
||||
"display_criteria.no_code_trigger": "Sem código",
|
||||
"display_criteria.overwritten": "Substituído",
|
||||
"display_criteria.randomizer": "Aleatorizador {percentage}%",
|
||||
"display_criteria.randomizer_description": "Apenas {percentage}% das pessoas que realizam a ação podem ser pesquisadas.",
|
||||
"display_criteria.recontact_description": "Opções de Recontacto",
|
||||
"display_criteria.targeted": "Alvo",
|
||||
"display_criteria.time_based_always": "Mostrar sempre o inquérito",
|
||||
"display_criteria.time_based_day": "Dia",
|
||||
"display_criteria.time_based_days": "Dias",
|
||||
"display_criteria.time_based_description": "Tempo de espera global",
|
||||
"display_criteria.trigger_description": "Desencadeador de Inquérito",
|
||||
"documentation_title": "Distribuir inquéritos de interceção em todas as plataformas",
|
||||
"html_embed": "HTML embutido em <head>",
|
||||
"ios_sdk": "SDK iOS para apps Apple",
|
||||
"javascript_sdk": "JavaScript SDK",
|
||||
"kotlin_sdk": "Kotlin SDK para aplicativos Android",
|
||||
"no_connection_description": "Ligue o seu website ou aplicação ao Formbricks para publicar inquéritos de intercepção.",
|
||||
"no_connection_title": "Ainda não está ligado!",
|
||||
"react_native_sdk": "SDK React Native para aplicações RN.",
|
||||
"title": "Configurações de interceptação de inquérito"
|
||||
},
|
||||
"includes_all": "Inclui tudo",
|
||||
"includes_either": "Inclui qualquer um",
|
||||
"install_widget": "Instalar Widget Formbricks",
|
||||
|
||||
@@ -1803,6 +1803,35 @@
|
||||
"go_to_setup_checklist": "前往設定檢查清單 \uD83D\uDC49",
|
||||
"impressions": "曝光數",
|
||||
"impressions_tooltip": "問卷已檢視的次數。",
|
||||
"in_app": {
|
||||
"connection_description": "調查將顯示給符合以下列出條件的網站用戶",
|
||||
"connection_title": "Formbricks SDK 已連線",
|
||||
"description": "Formbricks 調查 可以 嵌入 為 彈出 式 樣 式 , 根據 使用者 互動 。",
|
||||
"display_criteria": "顯示 的 標準",
|
||||
"display_criteria.audience_description": "目標受眾",
|
||||
"display_criteria.code_trigger": "程式 行動",
|
||||
"display_criteria.everyone": "所有人",
|
||||
"display_criteria.no_code_trigger": "無程式碼",
|
||||
"display_criteria.overwritten": "被覆寫",
|
||||
"display_criteria.randomizer": "{percentage} % 隨機器",
|
||||
"display_criteria.randomizer_description": "只有 {percentage}% 的人執行該動作後可能會被調查。",
|
||||
"display_criteria.recontact_description": "重新聯絡選項",
|
||||
"display_criteria.targeted": "目標",
|
||||
"display_criteria.time_based_always": "始終顯示問卷",
|
||||
"display_criteria.time_based_day": "天數",
|
||||
"display_criteria.time_based_days": "天數",
|
||||
"display_criteria.time_based_description": "全球等待時間",
|
||||
"display_criteria.trigger_description": "問卷 觸發器",
|
||||
"documentation_title": "在 所有 平台 上 發布 截取 調查",
|
||||
"html_embed": "<head> 中的 HTML 嵌入",
|
||||
"ios_sdk": "適用於 Apple 應用程式的 iOS SDK",
|
||||
"javascript_sdk": "JavaScript SDK",
|
||||
"kotlin_sdk": "適用於 Android 應用程式 的 Kotlin SDK",
|
||||
"no_connection_description": "將您的網站或應用程式與 Formbricks 連線以發布擷取調查。",
|
||||
"no_connection_title": "您尚未插入任何內容!",
|
||||
"react_native_sdk": "適用於 RN 應用程式的 React Native SDK",
|
||||
"title": "攔截 調查 設置"
|
||||
},
|
||||
"includes_all": "包含全部",
|
||||
"includes_either": "包含其中一個",
|
||||
"install_widget": "安裝 Formbricks 小工具",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { Button, ButtonProps } from "../button";
|
||||
|
||||
// Create a context to share variant and size with child components
|
||||
@@ -71,8 +71,10 @@ const Alert = React.forwardRef<
|
||||
>(({ className, variant, size, ...props }, ref) => {
|
||||
const variantIcon = variant && variant !== "default" ? alertVariantIcons[variant] : null;
|
||||
|
||||
const contextValue = useMemo(() => ({ variant, size }), [variant, size]);
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={{ variant, size }}>
|
||||
<AlertContext.Provider value={contextValue}>
|
||||
<div ref={ref} role="alert" className={cn(alertVariants({ variant, size }), className)} {...props}>
|
||||
{variantIcon}
|
||||
{props.children}
|
||||
|
||||
@@ -74,7 +74,7 @@ const DialogContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 md:zoom-in-90 data-[state=open]:md:slide-in-from-bottom-0 fixed z-50 flex max-h-[90dvh] w-full flex-col space-y-4 rounded-t-lg border bg-white p-4 shadow-lg sm:rounded-lg",
|
||||
!unconstrained && "sm:overflow-hidden",
|
||||
!unconstrained && "md:overflow-hidden",
|
||||
widthClass,
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { H1, H2, H3, H4, InlineCode, Large, Lead, List, Muted, P, Quote, Small } from "./index";
|
||||
import { H1, H2, H3, H4, InlineCode, InlineSmall, Large, Lead, List, Muted, P, Quote, Small } from "./index";
|
||||
|
||||
describe("Typography Components", () => {
|
||||
afterEach(() => {
|
||||
@@ -116,6 +116,16 @@ describe("Typography Components", () => {
|
||||
expect(codeElement?.className).toContain("font-semibold");
|
||||
});
|
||||
|
||||
test("renders InlineSmall correctly", () => {
|
||||
const { container } = render(<InlineSmall>Small inline text</InlineSmall>);
|
||||
const spanElement = container.querySelector("span");
|
||||
|
||||
expect(spanElement).toBeInTheDocument();
|
||||
expect(spanElement).toHaveTextContent("Small inline text");
|
||||
expect(spanElement?.className).toContain("text-sm");
|
||||
expect(spanElement?.className).toContain("font-normal");
|
||||
});
|
||||
|
||||
test("renders List correctly", () => {
|
||||
const { container } = render(
|
||||
<List>
|
||||
@@ -151,10 +161,33 @@ describe("Typography Components", () => {
|
||||
expect(h1Element).toHaveClass("text-4xl"); // Should still have default classes
|
||||
});
|
||||
|
||||
test("InlineSmall applies custom className correctly", () => {
|
||||
const { container } = render(
|
||||
<InlineSmall className="custom-inline-class">Custom small text</InlineSmall>
|
||||
);
|
||||
const spanElement = container.querySelector("span");
|
||||
|
||||
expect(spanElement).toHaveClass("custom-inline-class");
|
||||
expect(spanElement).toHaveClass("text-sm"); // Should still have default classes
|
||||
expect(spanElement).toHaveClass("font-normal");
|
||||
});
|
||||
|
||||
test("passes additional props to components", () => {
|
||||
const { container } = render(<H1 data-testid="test-heading">Test Heading</H1>);
|
||||
const h1Element = container.querySelector("h1");
|
||||
|
||||
expect(h1Element).toHaveAttribute("data-testid", "test-heading");
|
||||
});
|
||||
|
||||
test("InlineSmall passes additional props correctly", () => {
|
||||
const { container } = render(
|
||||
<InlineSmall data-testid="test-inline-small" title="Small text tooltip">
|
||||
Test Small
|
||||
</InlineSmall>
|
||||
);
|
||||
const spanElement = container.querySelector("span");
|
||||
|
||||
expect(spanElement).toHaveAttribute("data-testid", "test-inline-small");
|
||||
expect(spanElement).toHaveAttribute("title", "Small text tooltip");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,6 +143,17 @@ const Small = forwardRef<HTMLParagraphElement, SmallProps>((props, ref) => {
|
||||
Small.displayName = "Small";
|
||||
export { Small };
|
||||
|
||||
const InlineSmall = forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>((props, ref) => {
|
||||
return (
|
||||
<span {...props} ref={ref} className={cn("text-sm font-normal", props.className)}>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
InlineSmall.displayName = "InlineSmall";
|
||||
export { InlineSmall };
|
||||
|
||||
const Muted = forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>((props, ref) => {
|
||||
return (
|
||||
<span {...props} ref={ref} className={cn("text-muted-foreground text-sm", props.className)}>
|
||||
|
||||
Reference in New Issue
Block a user