chore: Added the tests to file upload summary (#5504)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
victorvhs017
2025-04-28 14:00:46 +07:00
committed by GitHub
parent 71fad1c22b
commit 8c1b9f81b9
15 changed files with 2126 additions and 3 deletions

View File

@@ -0,0 +1,154 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionSummaryAddress } from "@formbricks/types/surveys/types";
import { AddressSummary } from "./AddressSummary";
// Mock dependencies
vi.mock("@/lib/time", () => ({
timeSince: () => "2 hours ago",
}));
vi.mock("@/lib/utils/contact", () => ({
getContactIdentifier: () => "contact@example.com",
}));
vi.mock("@/modules/ui/components/avatars", () => ({
PersonAvatar: ({ personId }: { personId: string }) => <div data-testid="person-avatar">{personId}</div>,
}));
vi.mock("@/modules/ui/components/array-response", () => ({
ArrayResponse: ({ value }: { value: string[] }) => (
<div data-testid="array-response">{value.join(", ")}</div>
),
}));
vi.mock("./QuestionSummaryHeader", () => ({
QuestionSummaryHeader: () => <div data-testid="question-summary-header" />,
}));
describe("AddressSummary", () => {
afterEach(() => {
cleanup();
});
const environmentId = "env-123";
const survey = {} as TSurvey;
const locale = "en-US";
test("renders table headers correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Address Question" },
samples: [],
} as unknown as TSurveyQuestionSummaryAddress;
render(
<AddressSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByTestId("question-summary-header")).toBeInTheDocument();
expect(screen.getByText("common.user")).toBeInTheDocument();
expect(screen.getByText("common.response")).toBeInTheDocument();
expect(screen.getByText("common.time")).toBeInTheDocument();
});
test("renders contact information correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Address Question" },
samples: [
{
id: "response1",
value: ["123 Main St", "Apt 4", "New York", "NY", "10001"],
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: { email: "user@example.com" },
},
],
} as unknown as TSurveyQuestionSummaryAddress;
render(
<AddressSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByTestId("person-avatar")).toHaveTextContent("contact1");
expect(screen.getByText("contact@example.com")).toBeInTheDocument();
expect(screen.getByTestId("array-response")).toHaveTextContent("123 Main St, Apt 4, New York, NY, 10001");
expect(screen.getByText("2 hours ago")).toBeInTheDocument();
// Check link to contact
const contactLink = screen.getByText("contact@example.com").closest("a");
expect(contactLink).toHaveAttribute("href", `/environments/${environmentId}/contacts/contact1`);
});
test("renders anonymous user when no contact is provided", () => {
const questionSummary = {
question: { id: "q1", headline: "Address Question" },
samples: [
{
id: "response2",
value: ["456 Oak St", "London", "UK"],
updatedAt: new Date().toISOString(),
contact: null,
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryAddress;
render(
<AddressSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByTestId("person-avatar")).toHaveTextContent("anonymous");
expect(screen.getByText("common.anonymous")).toBeInTheDocument();
expect(screen.getByTestId("array-response")).toHaveTextContent("456 Oak St, London, UK");
});
test("renders multiple responses correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Address Question" },
samples: [
{
id: "response1",
value: ["123 Main St", "New York"],
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: {},
},
{
id: "response2",
value: ["456 Oak St", "London"],
updatedAt: new Date().toISOString(),
contact: { id: "contact2" },
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryAddress;
render(
<AddressSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getAllByTestId("person-avatar")).toHaveLength(2);
expect(screen.getAllByTestId("array-response")).toHaveLength(2);
expect(screen.getAllByText("2 hours ago")).toHaveLength(2);
});
});

View File

@@ -0,0 +1,89 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionSummaryCta } from "@formbricks/types/surveys/types";
import { CTASummary } from "./CTASummary";
vi.mock("@/modules/ui/components/progress-bar", () => ({
ProgressBar: ({ progress, barColor }: { progress: number; barColor: string }) => (
<div data-testid="progress-bar">{`${progress}-${barColor}`}</div>
),
}));
vi.mock("./QuestionSummaryHeader", () => ({
QuestionSummaryHeader: ({
additionalInfo,
}: {
showResponses: boolean;
additionalInfo: React.ReactNode;
}) => <div data-testid="question-summary-header">{additionalInfo}</div>,
}));
vi.mock("lucide-react", () => ({
InboxIcon: () => <div data-testid="inbox-icon" />,
}));
vi.mock("../lib/utils", () => ({
convertFloatToNDecimal: (value: number) => value.toFixed(2),
}));
describe("CTASummary", () => {
afterEach(() => {
cleanup();
});
const survey = {} as TSurvey;
test("renders with all metrics and required question", () => {
const questionSummary = {
question: { id: "q1", headline: "CTA Question", required: true },
impressionCount: 100,
clickCount: 25,
skipCount: 10,
ctr: { count: 25, percentage: 25 },
} as unknown as TSurveyQuestionSummaryCta;
render(<CTASummary questionSummary={questionSummary} survey={survey} />);
expect(screen.getByTestId("question-summary-header")).toBeInTheDocument();
expect(screen.getByText("100 common.impressions")).toBeInTheDocument();
// Use getAllByText instead of getByText for multiple matching elements
expect(screen.getAllByText("25 common.clicks")).toHaveLength(2);
expect(screen.queryByText("10 common.skips")).not.toBeInTheDocument(); // Should not show skips for required questions
// Check CTR section
expect(screen.getByText("CTR")).toBeInTheDocument();
expect(screen.getByText("25.00%")).toBeInTheDocument();
// Check progress bar
expect(screen.getByTestId("progress-bar")).toHaveTextContent("0.25-bg-brand-dark");
});
test("renders skip count for non-required questions", () => {
const questionSummary = {
question: { id: "q1", headline: "CTA Question", required: false },
impressionCount: 100,
clickCount: 20,
skipCount: 30,
ctr: { count: 20, percentage: 20 },
} as unknown as TSurveyQuestionSummaryCta;
render(<CTASummary questionSummary={questionSummary} survey={survey} />);
expect(screen.getByText("30 common.skips")).toBeInTheDocument();
});
test("renders singular form for count = 1", () => {
const questionSummary = {
question: { id: "q1", headline: "CTA Question", required: true },
impressionCount: 10,
clickCount: 1,
skipCount: 0,
ctr: { count: 1, percentage: 10 },
} as unknown as TSurveyQuestionSummaryCta;
render(<CTASummary questionSummary={questionSummary} survey={survey} />);
// Use getAllByText instead of getByText for multiple matching elements
expect(screen.getAllByText("1 common.click")).toHaveLength(1);
});
});

View File

@@ -0,0 +1,69 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionSummaryCal } from "@formbricks/types/surveys/types";
import { CalSummary } from "./CalSummary";
vi.mock("@/modules/ui/components/progress-bar", () => ({
ProgressBar: ({ progress, barColor }: { progress: number; barColor: string }) => (
<div data-testid="progress-bar">{`${progress}-${barColor}`}</div>
),
}));
vi.mock("./QuestionSummaryHeader", () => ({
QuestionSummaryHeader: () => <div data-testid="question-summary-header" />,
}));
vi.mock("../lib/utils", () => ({
convertFloatToNDecimal: (value: number) => value.toFixed(2),
}));
describe("CalSummary", () => {
afterEach(() => {
cleanup();
});
const environmentId = "env-123";
const survey = {} as TSurvey;
test("renders the correct components and data", () => {
const questionSummary = {
question: { id: "q1", headline: "Calendar Question" },
booked: { count: 5, percentage: 75 },
skipped: { count: 1, percentage: 25 },
} as unknown as TSurveyQuestionSummaryCal;
render(<CalSummary questionSummary={questionSummary} environmentId={environmentId} survey={survey} />);
expect(screen.getByTestId("question-summary-header")).toBeInTheDocument();
// Check if booked section is displayed
expect(screen.getByText("common.booked")).toBeInTheDocument();
expect(screen.getByText("75.00%")).toBeInTheDocument();
expect(screen.getByText("5 common.responses")).toBeInTheDocument();
// Check if skipped section is displayed
expect(screen.getByText("common.dismissed")).toBeInTheDocument();
expect(screen.getByText("25.00%")).toBeInTheDocument();
expect(screen.getByText("1 common.response")).toBeInTheDocument();
// Check progress bars
const progressBars = screen.getAllByTestId("progress-bar");
expect(progressBars).toHaveLength(2);
expect(progressBars[0]).toHaveTextContent("0.75-bg-brand-dark");
expect(progressBars[1]).toHaveTextContent("0.25-bg-brand-dark");
});
test("renders singular and plural response counts correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Calendar Question" },
booked: { count: 1, percentage: 50 },
skipped: { count: 1, percentage: 50 },
} as unknown as TSurveyQuestionSummaryCal;
render(<CalSummary questionSummary={questionSummary} environmentId={environmentId} survey={survey} />);
// Use getAllByText directly since we know there are multiple matching elements
const responseElements = screen.getAllByText("1 common.response");
expect(responseElements).toHaveLength(2);
});
});

View File

@@ -0,0 +1,153 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionSummaryContactInfo } from "@formbricks/types/surveys/types";
import { ContactInfoSummary } from "./ContactInfoSummary";
vi.mock("@/lib/time", () => ({
timeSince: () => "2 hours ago",
}));
vi.mock("@/lib/utils/contact", () => ({
getContactIdentifier: () => "contact@example.com",
}));
vi.mock("@/modules/ui/components/avatars", () => ({
PersonAvatar: ({ personId }: { personId: string }) => <div data-testid="person-avatar">{personId}</div>,
}));
vi.mock("@/modules/ui/components/array-response", () => ({
ArrayResponse: ({ value }: { value: string[] }) => (
<div data-testid="array-response">{value.join(", ")}</div>
),
}));
vi.mock("./QuestionSummaryHeader", () => ({
QuestionSummaryHeader: () => <div data-testid="question-summary-header" />,
}));
describe("ContactInfoSummary", () => {
afterEach(() => {
cleanup();
});
const environmentId = "env-123";
const survey = {} as TSurvey;
const locale = "en-US";
test("renders table headers correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Contact Info Question" },
samples: [],
} as unknown as TSurveyQuestionSummaryContactInfo;
render(
<ContactInfoSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByTestId("question-summary-header")).toBeInTheDocument();
expect(screen.getByText("common.user")).toBeInTheDocument();
expect(screen.getByText("common.response")).toBeInTheDocument();
expect(screen.getByText("common.time")).toBeInTheDocument();
});
test("renders contact information correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Contact Info Question" },
samples: [
{
id: "response1",
value: ["John Doe", "john@example.com", "+1234567890"],
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: { email: "user@example.com" },
},
],
} as unknown as TSurveyQuestionSummaryContactInfo;
render(
<ContactInfoSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByTestId("person-avatar")).toHaveTextContent("contact1");
expect(screen.getByText("contact@example.com")).toBeInTheDocument();
expect(screen.getByTestId("array-response")).toHaveTextContent("John Doe, john@example.com, +1234567890");
expect(screen.getByText("2 hours ago")).toBeInTheDocument();
// Check link to contact
const contactLink = screen.getByText("contact@example.com").closest("a");
expect(contactLink).toHaveAttribute("href", `/environments/${environmentId}/contacts/contact1`);
});
test("renders anonymous user when no contact is provided", () => {
const questionSummary = {
question: { id: "q1", headline: "Contact Info Question" },
samples: [
{
id: "response2",
value: ["Anonymous User", "anonymous@example.com"],
updatedAt: new Date().toISOString(),
contact: null,
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryContactInfo;
render(
<ContactInfoSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByTestId("person-avatar")).toHaveTextContent("anonymous");
expect(screen.getByText("common.anonymous")).toBeInTheDocument();
expect(screen.getByTestId("array-response")).toHaveTextContent("Anonymous User, anonymous@example.com");
});
test("renders multiple responses correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Contact Info Question" },
samples: [
{
id: "response1",
value: ["John Doe", "john@example.com"],
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: {},
},
{
id: "response2",
value: ["Jane Smith", "jane@example.com"],
updatedAt: new Date().toISOString(),
contact: { id: "contact2" },
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryContactInfo;
render(
<ContactInfoSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getAllByTestId("person-avatar")).toHaveLength(2);
expect(screen.getAllByTestId("array-response")).toHaveLength(2);
expect(screen.getAllByText("2 hours ago")).toHaveLength(2);
});
});

View File

@@ -0,0 +1,192 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionSummaryDate } from "@formbricks/types/surveys/types";
import { DateQuestionSummary } from "./DateQuestionSummary";
vi.mock("@/lib/time", () => ({
timeSince: () => "2 hours ago",
}));
vi.mock("@/lib/utils/contact", () => ({
getContactIdentifier: () => "contact@example.com",
}));
vi.mock("@/lib/utils/datetime", () => ({
formatDateWithOrdinal: (_: Date) => "January 1st, 2023",
}));
vi.mock("@/modules/ui/components/avatars", () => ({
PersonAvatar: ({ personId }: { personId: string }) => <div data-testid="person-avatar">{personId}</div>,
}));
vi.mock("@/modules/ui/components/button", () => ({
Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => (
<button onClick={onClick} data-testid="load-more-button">
{children}
</button>
),
}));
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
<a href={href} data-testid="next-link">
{children}
</a>
),
}));
vi.mock("./QuestionSummaryHeader", () => ({
QuestionSummaryHeader: () => <div data-testid="question-summary-header" />,
}));
describe("DateQuestionSummary", () => {
afterEach(() => {
cleanup();
});
const environmentId = "env-123";
const survey = {} as TSurvey;
const locale = "en-US";
test("renders table headers correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Date Question" },
samples: [],
} as unknown as TSurveyQuestionSummaryDate;
render(
<DateQuestionSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByTestId("question-summary-header")).toBeInTheDocument();
expect(screen.getByText("common.user")).toBeInTheDocument();
expect(screen.getByText("common.response")).toBeInTheDocument();
expect(screen.getByText("common.time")).toBeInTheDocument();
});
test("renders date responses correctly", () => {
const questionSummary = {
question: { id: "q1", headline: "Date Question" },
samples: [
{
id: "response1",
value: "2023-01-01",
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryDate;
render(
<DateQuestionSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByText("January 1st, 2023")).toBeInTheDocument();
expect(screen.getByText("contact@example.com")).toBeInTheDocument();
expect(screen.getByText("2 hours ago")).toBeInTheDocument();
});
test("renders invalid dates with special message", () => {
const questionSummary = {
question: { id: "q1", headline: "Date Question" },
samples: [
{
id: "response1",
value: "invalid-date",
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryDate;
render(
<DateQuestionSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByText("common.invalid_date(invalid-date)")).toBeInTheDocument();
});
test("renders anonymous user when no contact is provided", () => {
const questionSummary = {
question: { id: "q1", headline: "Date Question" },
samples: [
{
id: "response1",
value: "2023-01-01",
updatedAt: new Date().toISOString(),
contact: null,
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryDate;
render(
<DateQuestionSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByText("common.anonymous")).toBeInTheDocument();
});
test("shows load more button when there are more responses and loads more on click", async () => {
const samples = Array.from({ length: 15 }, (_, i) => ({
id: `response${i}`,
value: "2023-01-01",
updatedAt: new Date().toISOString(),
contact: null,
contactAttributes: {},
}));
const questionSummary = {
question: { id: "q1", headline: "Date Question" },
samples,
} as unknown as TSurveyQuestionSummaryDate;
render(
<DateQuestionSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
// Initially 10 responses should be visible
expect(screen.getAllByText("January 1st, 2023")).toHaveLength(10);
// "Load More" button should be visible
const loadMoreButton = screen.getByTestId("load-more-button");
expect(loadMoreButton).toBeInTheDocument();
// Click "Load More"
await userEvent.click(loadMoreButton);
// Now all 15 responses should be visible
expect(screen.getAllByText("January 1st, 2023")).toHaveLength(15);
// "Load More" button should disappear
expect(screen.queryByTestId("load-more-button")).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,153 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import toast from "react-hot-toast";
import { afterEach, describe, expect, test, vi } from "vitest";
import { EnableInsightsBanner } from "./EnableInsightsBanner";
vi.mock("react-hot-toast", () => ({
default: {
success: vi.fn(),
},
}));
vi.mock("@/modules/ee/insights/actions", () => ({
generateInsightsForSurveyAction: vi.fn(),
}));
vi.mock("@/modules/ui/components/alert", () => ({
Alert: ({ children, className }: { children: React.ReactNode; className: string }) => (
<div data-testid="alert" className={className}>
{children}
</div>
),
AlertTitle: ({ children }: { children: React.ReactNode }) => (
<div data-testid="alert-title">{children}</div>
),
AlertDescription: ({ children, className }: { children: React.ReactNode; className: string }) => (
<div data-testid="alert-description" className={className}>
{children}
</div>
),
}));
vi.mock("@/modules/ui/components/badge", () => ({
Badge: ({ type, size, text }: { type: string; size: string; text: string }) => (
<span data-testid="badge" data-type={type} data-size={size}>
{text}
</span>
),
}));
vi.mock("@/modules/ui/components/button", () => ({
Button: ({
size,
className,
onClick,
loading,
disabled,
children,
}: {
size: string;
className: string;
onClick: () => void;
loading: boolean;
disabled: boolean;
children: React.ReactNode;
}) => (
<button
data-testid="button"
data-size={size}
className={className}
onClick={onClick}
disabled={disabled || loading}
aria-busy={loading}>
{children}
</button>
),
}));
vi.mock("@/modules/ui/components/tooltip", () => ({
TooltipRenderer: ({
tooltipContent,
children,
}: {
tooltipContent: string | undefined;
children: React.ReactNode;
}) => (
<div data-testid="tooltip" data-content={tooltipContent}>
{children}
</div>
),
}));
vi.mock("lucide-react", () => ({
SparklesIcon: ({ className, strokeWidth }: { className: string; strokeWidth: number }) => (
<div data-testid="sparkles-icon" className={className} data-stroke-width={strokeWidth} />
),
}));
describe("EnableInsightsBanner", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const surveyId = "survey-123";
test("renders banner with correct content", () => {
render(<EnableInsightsBanner surveyId={surveyId} maxResponseCount={100} surveyResponseCount={50} />);
expect(screen.getByTestId("alert")).toBeInTheDocument();
expect(screen.getByTestId("alert-title")).toBeInTheDocument();
expect(screen.getByTestId("badge")).toHaveTextContent("Beta");
expect(screen.getByTestId("alert-description")).toBeInTheDocument();
expect(screen.getByTestId("sparkles-icon")).toBeInTheDocument();
expect(screen.getByTestId("button")).toBeInTheDocument();
expect(
screen.getByText("environments.surveys.summary.enable_ai_insights_banner_button")
).toBeInTheDocument();
});
test("disables button when response count exceeds maximum", () => {
render(<EnableInsightsBanner surveyId={surveyId} maxResponseCount={50} surveyResponseCount={100} />);
const button = screen.getByTestId("button");
expect(button).toBeDisabled();
// Tooltip should have content when button is disabled
const tooltip = screen.getByTestId("tooltip");
expect(tooltip).toHaveAttribute(
"data-content",
"environments.surveys.summary.enable_ai_insights_banner_tooltip"
);
});
test("enables button when response count is within maximum", () => {
render(<EnableInsightsBanner surveyId={surveyId} maxResponseCount={100} surveyResponseCount={50} />);
const button = screen.getByTestId("button");
expect(button).not.toBeDisabled();
// Tooltip should not have content when button is enabled
const tooltip = screen.getByTestId("tooltip");
expect(tooltip).not.toHaveAttribute(
"data-content",
"environments.surveys.summary.enable_ai_insights_banner_tooltip"
);
});
test("generates insights when button is clicked", async () => {
const { generateInsightsForSurveyAction } = await import("@/modules/ee/insights/actions");
render(<EnableInsightsBanner surveyId={surveyId} maxResponseCount={100} surveyResponseCount={50} />);
const button = screen.getByTestId("button");
await userEvent.click(button);
expect(toast.success).toHaveBeenCalledTimes(2);
expect(generateInsightsForSurveyAction).toHaveBeenCalledWith({ surveyId });
// Banner should disappear after generating insights
expect(screen.queryByTestId("alert")).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,231 @@
import { FileUploadSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import {
TSurvey,
TSurveyFileUploadQuestion,
TSurveyQuestionSummaryFileUpload,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
// Mock child components and hooks
vi.mock("@/modules/ui/components/avatars", () => ({
PersonAvatar: vi.fn(() => <div>PersonAvatarMock</div>),
}));
vi.mock("./QuestionSummaryHeader", () => ({
QuestionSummaryHeader: vi.fn(() => <div>QuestionSummaryHeaderMock</div>),
}));
// Mock utility functions
vi.mock("@/lib/storage/utils", () => ({
getOriginalFileNameFromUrl: (url: string) => `original-${url.split("/").pop()}`,
}));
vi.mock("@/lib/time", () => ({
timeSince: () => "some time ago",
}));
vi.mock("@/lib/utils/contact", () => ({
getContactIdentifier: () => "contact@example.com",
}));
const environmentId = "test-env-id";
const survey = { id: "survey-1" } as TSurvey;
const locale = "en-US";
const createMockResponse = (id: string, value: string[], contactId: string | null = null) => ({
id: `response-${id}`,
value,
updatedAt: new Date().toISOString(),
contact: contactId ? { id: contactId, name: `Contact ${contactId}` } : null,
contactAttributes: contactId ? { email: `contact${contactId}@example.com` } : {},
});
const questionSummaryBase = {
question: {
id: "q1",
headline: { default: "Upload your file" },
type: TSurveyQuestionTypeEnum.FileUpload,
} as unknown as TSurveyFileUploadQuestion,
responseCount: 0,
files: [],
} as unknown as TSurveyQuestionSummaryFileUpload;
describe("FileUploadSummary", () => {
afterEach(() => {
cleanup();
});
test("renders the component with initial responses", () => {
const files = Array.from({ length: 5 }, (_, i) =>
createMockResponse(i.toString(), [`https://example.com/file${i}.pdf`], `contact-${i}`)
);
const questionSummary = {
...questionSummaryBase,
files,
responseCount: files.length,
} as unknown as TSurveyQuestionSummaryFileUpload;
render(
<FileUploadSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByText("QuestionSummaryHeaderMock")).toBeInTheDocument();
expect(screen.getByText("common.user")).toBeInTheDocument();
expect(screen.getByText("common.response")).toBeInTheDocument();
expect(screen.getByText("common.time")).toBeInTheDocument();
expect(screen.getAllByText("PersonAvatarMock")).toHaveLength(5);
expect(screen.getAllByText("contact@example.com")).toHaveLength(5);
expect(screen.getByText("original-file0.pdf")).toBeInTheDocument();
expect(screen.getByText("original-file4.pdf")).toBeInTheDocument();
expect(screen.queryByText("common.load_more")).not.toBeInTheDocument();
});
test("renders 'Skipped' when value is an empty array", () => {
const files = [createMockResponse("skipped", [], "contact-skipped")];
const questionSummary = {
...questionSummaryBase,
files,
responseCount: files.length,
} as unknown as TSurveyQuestionSummaryFileUpload;
render(
<FileUploadSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByText("common.skipped")).toBeInTheDocument();
expect(screen.queryByText(/original-/)).not.toBeInTheDocument(); // No file name should be rendered
});
test("renders 'Anonymous' when contact is null", () => {
const files = [createMockResponse("anon", ["https://example.com/anonfile.jpg"], null)];
const questionSummary = {
...questionSummaryBase,
files,
responseCount: files.length,
} as unknown as TSurveyQuestionSummaryFileUpload;
render(
<FileUploadSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByText("common.anonymous")).toBeInTheDocument();
expect(screen.getByText("original-anonfile.jpg")).toBeInTheDocument();
});
test("shows 'Load More' button when there are more than 10 responses and loads more on click", async () => {
const files = Array.from({ length: 15 }, (_, i) =>
createMockResponse(i.toString(), [`https://example.com/file${i}.txt`], `contact-${i}`)
);
const questionSummary = {
...questionSummaryBase,
files,
responseCount: files.length,
} as unknown as TSurveyQuestionSummaryFileUpload;
render(
<FileUploadSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
// Initially 10 responses should be visible
expect(screen.getAllByText("PersonAvatarMock")).toHaveLength(10);
expect(screen.getByText("original-file9.txt")).toBeInTheDocument();
expect(screen.queryByText("original-file10.txt")).not.toBeInTheDocument();
// "Load More" button should be visible
const loadMoreButton = screen.getByText("common.load_more");
expect(loadMoreButton).toBeInTheDocument();
// Click "Load More"
await userEvent.click(loadMoreButton);
// Now all 15 responses should be visible
expect(screen.getAllByText("PersonAvatarMock")).toHaveLength(15);
expect(screen.getByText("original-file14.txt")).toBeInTheDocument();
// "Load More" button should disappear
expect(screen.queryByText("common.load_more")).not.toBeInTheDocument();
});
test("renders multiple files for a single response", () => {
const files = [
createMockResponse(
"multi",
["https://example.com/fileA.png", "https://example.com/fileB.docx"],
"contact-multi"
),
];
const questionSummary = {
...questionSummaryBase,
files,
responseCount: files.length,
} as unknown as TSurveyQuestionSummaryFileUpload;
render(
<FileUploadSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
expect(screen.getByText("original-fileA.png")).toBeInTheDocument();
expect(screen.getByText("original-fileB.docx")).toBeInTheDocument();
// Check that download links exist
const links = screen.getAllByRole("link");
// 1 contact link + 2 file links
expect(links.filter((link) => link.getAttribute("target") === "_blank")).toHaveLength(2);
expect(
links.find((link) => link.getAttribute("href") === "https://example.com/fileA.png")
).toBeInTheDocument();
expect(
links.find((link) => link.getAttribute("href") === "https://example.com/fileB.docx")
).toBeInTheDocument();
});
test("renders contact link correctly", () => {
const contactId = "contact-link-test";
const files = [createMockResponse("link", ["https://example.com/link.pdf"], contactId)];
const questionSummary = {
...questionSummaryBase,
files,
responseCount: files.length,
} as unknown as TSurveyQuestionSummaryFileUpload;
render(
<FileUploadSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
locale={locale}
/>
);
const contactLink = screen.getByText("contact@example.com").closest("a");
expect(contactLink).toBeInTheDocument();
expect(contactLink).toHaveAttribute("href", `/environments/${environmentId}/contacts/${contactId}`);
});
});

View File

@@ -74,12 +74,12 @@ export const FileUploadSummary = ({
<div className="col-span-2 grid">
{Array.isArray(response.value) &&
(response.value.length > 0 ? (
response.value.map((fileUrl, index) => {
response.value.map((fileUrl) => {
const fileName = getOriginalFileNameFromUrl(fileUrl);
return (
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
<a href={fileUrl} key={index} target="_blank" rel="noopener noreferrer">
<a href={fileUrl} key={fileUrl} target="_blank" rel="noopener noreferrer">
<div className="absolute top-0 right-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
<DownloadIcon className="h-6 text-slate-500" />

View File

@@ -0,0 +1,183 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurveyQuestionSummaryHiddenFields } from "@formbricks/types/surveys/types";
import { HiddenFieldsSummary } from "./HiddenFieldsSummary";
// Mock dependencies
vi.mock("@/lib/time", () => ({
timeSince: () => "2 hours ago",
}));
vi.mock("@/lib/utils/contact", () => ({
getContactIdentifier: () => "contact@example.com",
}));
vi.mock("@/modules/ui/components/avatars", () => ({
PersonAvatar: ({ personId }: { personId: string }) => <div data-testid="person-avatar">{personId}</div>,
}));
vi.mock("@/modules/ui/components/button", () => ({
Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => (
<button onClick={onClick} data-testid="load-more-button">
{children}
</button>
),
}));
// Mock lucide-react components
vi.mock("lucide-react", () => ({
InboxIcon: () => <div data-testid="inbox-icon" />,
MessageSquareTextIcon: () => <div data-testid="message-icon" />,
Link: ({ children, href, className }: { children: React.ReactNode; href: string; className: string }) => (
<a href={href} className={className} data-testid="lucide-link">
{children}
</a>
),
}));
// Mock Next.js Link
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
<a href={href} data-testid="next-link">
{children}
</a>
),
}));
describe("HiddenFieldsSummary", () => {
afterEach(() => {
cleanup();
});
const environment = { id: "env-123" } as TEnvironment;
const locale = "en-US";
test("renders component with correct header and single response", () => {
const questionSummary = {
id: "hidden-field-1",
responseCount: 1,
samples: [
{
id: "response1",
value: "Hidden value",
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryHiddenFields;
render(
<HiddenFieldsSummary environment={environment} questionSummary={questionSummary} locale={locale} />
);
expect(screen.getByText("hidden-field-1")).toBeInTheDocument();
expect(screen.getByText("Hidden Field")).toBeInTheDocument();
expect(screen.getByText("1 common.response")).toBeInTheDocument();
// Headers
expect(screen.getByText("common.user")).toBeInTheDocument();
expect(screen.getByText("common.response")).toBeInTheDocument();
expect(screen.getByText("common.time")).toBeInTheDocument();
// We can skip checking for PersonAvatar as it's inside hidden md:flex
expect(screen.getByText("contact@example.com")).toBeInTheDocument();
expect(screen.getByText("Hidden value")).toBeInTheDocument();
expect(screen.getByText("2 hours ago")).toBeInTheDocument();
// Check for link without checking for specific href
expect(screen.getByText("contact@example.com")).toBeInTheDocument();
});
test("renders anonymous user when no contact is provided", () => {
const questionSummary = {
id: "hidden-field-1",
responseCount: 1,
samples: [
{
id: "response1",
value: "Anonymous hidden value",
updatedAt: new Date().toISOString(),
contact: null,
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryHiddenFields;
render(
<HiddenFieldsSummary environment={environment} questionSummary={questionSummary} locale={locale} />
);
// Instead of checking for avatar, just check for anonymous text
expect(screen.getByText("common.anonymous")).toBeInTheDocument();
expect(screen.getByText("Anonymous hidden value")).toBeInTheDocument();
});
test("renders plural response label when multiple responses", () => {
const questionSummary = {
id: "hidden-field-1",
responseCount: 2,
samples: [
{
id: "response1",
value: "Hidden value 1",
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: {},
},
{
id: "response2",
value: "Hidden value 2",
updatedAt: new Date().toISOString(),
contact: { id: "contact2" },
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryHiddenFields;
render(
<HiddenFieldsSummary environment={environment} questionSummary={questionSummary} locale={locale} />
);
expect(screen.getByText("2 common.responses")).toBeInTheDocument();
expect(screen.getAllByText("contact@example.com")).toHaveLength(2);
});
test("shows load more button when there are more responses and loads more on click", async () => {
const samples = Array.from({ length: 15 }, (_, i) => ({
id: `response${i}`,
value: `Hidden value ${i}`,
updatedAt: new Date().toISOString(),
contact: null,
contactAttributes: {},
}));
const questionSummary = {
id: "hidden-field-1",
responseCount: samples.length,
samples,
} as unknown as TSurveyQuestionSummaryHiddenFields;
render(
<HiddenFieldsSummary environment={environment} questionSummary={questionSummary} locale={locale} />
);
// Initially 10 responses should be visible
expect(screen.getAllByText(/Hidden value \d+/)).toHaveLength(10);
// "Load More" button should be visible
const loadMoreButton = screen.getByTestId("load-more-button");
expect(loadMoreButton).toBeInTheDocument();
// Click "Load More"
await userEvent.click(loadMoreButton);
// Now all 15 responses should be visible
expect(screen.getAllByText(/Hidden value \d+/)).toHaveLength(15);
// "Load More" button should disappear
expect(screen.queryByTestId("load-more-button")).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,265 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionSummaryOpenText } from "@formbricks/types/surveys/types";
import { OpenTextSummary } from "./OpenTextSummary";
// Mock dependencies
vi.mock("@/lib/time", () => ({
timeSince: () => "2 hours ago",
}));
vi.mock("@/lib/utils/contact", () => ({
getContactIdentifier: () => "contact@example.com",
}));
vi.mock("@/modules/analysis/utils", () => ({
renderHyperlinkedContent: (text: string) => <div data-testid="hyperlinked-content">{text}</div>,
}));
vi.mock("@/modules/ui/components/avatars", () => ({
PersonAvatar: ({ personId }: { personId: string }) => <div data-testid="person-avatar">{personId}</div>,
}));
vi.mock("@/modules/ui/components/button", () => ({
Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => (
<button onClick={onClick} data-testid="load-more-button">
{children}
</button>
),
}));
vi.mock("@/modules/ui/components/secondary-navigation", () => ({
SecondaryNavigation: ({ activeId, navigation }: any) => (
<div data-testid="secondary-navigation">
{navigation.map((item: any) => (
<button key={item.id} onClick={item.onClick} data-active={activeId === item.id}>
{item.label}
</button>
))}
</div>
),
}));
vi.mock("@/modules/ui/components/table", () => ({
Table: ({ children }: { children: React.ReactNode }) => <table data-testid="table">{children}</table>,
TableHeader: ({ children }: { children: React.ReactNode }) => <thead>{children}</thead>,
TableBody: ({ children }: { children: React.ReactNode }) => <tbody>{children}</tbody>,
TableRow: ({ children }: { children: React.ReactNode }) => <tr>{children}</tr>,
TableHead: ({ children }: { children: React.ReactNode }) => <th>{children}</th>,
TableCell: ({ children, width }: { children: React.ReactNode; width?: number }) => (
<td style={width ? { width } : {}}>{children}</td>
),
}));
vi.mock("@/modules/ee/insights/components/insights-view", () => ({
InsightView: () => <div data-testid="insight-view"></div>,
}));
vi.mock("./QuestionSummaryHeader", () => ({
QuestionSummaryHeader: ({ additionalInfo }: { additionalInfo?: React.ReactNode }) => (
<div data-testid="question-summary-header">{additionalInfo}</div>
),
}));
describe("OpenTextSummary", () => {
afterEach(() => {
cleanup();
});
const environmentId = "env-123";
const survey = { id: "survey-1" } as TSurvey;
const locale = "en-US";
test("renders response mode by default when insights not enabled", () => {
const questionSummary = {
question: { id: "q1", headline: "Open Text Question" },
insightsEnabled: false,
insights: [],
samples: [
{
id: "response1",
value: "Sample response text",
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryOpenText;
render(
<OpenTextSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
isAIEnabled={true}
locale={locale}
/>
);
expect(screen.getByTestId("question-summary-header")).toBeInTheDocument();
expect(screen.getByTestId("table")).toBeInTheDocument();
expect(screen.getByTestId("person-avatar")).toHaveTextContent("contact1");
expect(screen.getByText("contact@example.com")).toBeInTheDocument();
expect(screen.getByTestId("hyperlinked-content")).toHaveTextContent("Sample response text");
expect(screen.getByText("2 hours ago")).toBeInTheDocument();
// No secondary navigation when insights not enabled
expect(screen.queryByTestId("secondary-navigation")).not.toBeInTheDocument();
});
test("shows insights disabled message when AI is enabled but insights are disabled", () => {
const questionSummary = {
question: { id: "q1", headline: "Open Text Question" },
insightsEnabled: false,
insights: [],
samples: [],
} as unknown as TSurveyQuestionSummaryOpenText;
render(
<OpenTextSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
isAIEnabled={true}
locale={locale}
/>
);
expect(screen.getByText("environments.surveys.summary.insights_disabled")).toBeInTheDocument();
});
test("shows insights tab by default when insights are available", () => {
const questionSummary = {
question: { id: "q1", headline: "Open Text Question" },
insightsEnabled: true,
insights: [{ id: "insight1", text: "Insight text" }],
samples: [],
} as unknown as TSurveyQuestionSummaryOpenText;
render(
<OpenTextSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
isAIEnabled={true}
locale={locale}
/>
);
expect(screen.getByTestId("secondary-navigation")).toBeInTheDocument();
expect(screen.getByTestId("insight-view")).toBeInTheDocument();
expect(screen.queryByTestId("table")).not.toBeInTheDocument();
});
test("allows switching between insights and responses tabs", async () => {
const questionSummary = {
question: { id: "q1", headline: "Open Text Question" },
insightsEnabled: true,
insights: [{ id: "insight1", text: "Insight text" }],
samples: [
{
id: "response1",
value: "Sample response text",
updatedAt: new Date().toISOString(),
contact: { id: "contact1" },
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryOpenText;
render(
<OpenTextSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
isAIEnabled={true}
locale={locale}
/>
);
// Initially showing insights
expect(screen.getByTestId("insight-view")).toBeInTheDocument();
expect(screen.queryByTestId("table")).not.toBeInTheDocument();
// Click on responses tab
await userEvent.click(screen.getByText("common.responses"));
// Now showing responses
expect(screen.queryByTestId("insight-view")).not.toBeInTheDocument();
expect(screen.getByTestId("table")).toBeInTheDocument();
});
test("renders anonymous user when no contact is provided", () => {
const questionSummary = {
question: { id: "q1", headline: "Open Text Question" },
insightsEnabled: false,
insights: [],
samples: [
{
id: "response1",
value: "Anonymous response",
updatedAt: new Date().toISOString(),
contact: null,
contactAttributes: {},
},
],
} as unknown as TSurveyQuestionSummaryOpenText;
render(
<OpenTextSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
isAIEnabled={false}
locale={locale}
/>
);
expect(screen.getByTestId("person-avatar")).toHaveTextContent("anonymous");
expect(screen.getByText("common.anonymous")).toBeInTheDocument();
});
test("shows load more button when there are more responses and loads more on click", async () => {
const samples = Array.from({ length: 15 }, (_, i) => ({
id: `response${i}`,
value: `Response ${i}`,
updatedAt: new Date().toISOString(),
contact: null,
contactAttributes: {},
}));
const questionSummary = {
question: { id: "q1", headline: "Open Text Question" },
insightsEnabled: false,
insights: [],
samples,
} as unknown as TSurveyQuestionSummaryOpenText;
render(
<OpenTextSummary
questionSummary={questionSummary}
environmentId={environmentId}
survey={survey}
isAIEnabled={false}
locale={locale}
/>
);
// Initially 10 responses should be visible
expect(screen.getAllByTestId("hyperlinked-content")).toHaveLength(10);
// "Load More" button should be visible
const loadMoreButton = screen.getByTestId("load-more-button");
expect(loadMoreButton).toBeInTheDocument();
// Click "Load More"
await userEvent.click(loadMoreButton);
// Now all 15 responses should be visible
expect(screen.getAllByTestId("hyperlinked-content")).toHaveLength(15);
// "Load More" button should disappear
expect(screen.queryByTestId("load-more-button")).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,164 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionSummary, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
// Mock dependencies
vi.mock("@/lib/utils/recall", () => ({
recallToHeadline: () => ({ default: "Recalled Headline" }),
}));
vi.mock("@/modules/survey/editor/lib/utils", () => ({
formatTextWithSlashes: (text: string) => <span data-testid="formatted-headline">{text}</span>,
}));
vi.mock("@/modules/survey/lib/questions", () => ({
getQuestionTypes: () => [
{
id: "openText",
label: "Open Text",
icon: () => <div data-testid="question-icon">Icon</div>,
},
{
id: "multipleChoice",
label: "Multiple Choice",
icon: () => <div data-testid="question-icon">Icon</div>,
},
],
}));
vi.mock("@/modules/ui/components/settings-id", () => ({
SettingsId: ({ title, id }: { title: string; id: string }) => (
<div data-testid="settings-id">
{title}: {id}
</div>
),
}));
// Mock InboxIcon
vi.mock("lucide-react", () => ({
InboxIcon: () => <div data-testid="inbox-icon"></div>,
}));
describe("QuestionSummaryHeader", () => {
afterEach(() => {
cleanup();
});
const survey = {} as TSurvey;
test("renders header with question headline and type", () => {
const questionSummary = {
question: {
id: "q1",
headline: { default: "Test Question" },
type: "openText" as TSurveyQuestionTypeEnum,
required: true,
},
responseCount: 42,
} as unknown as TSurveyQuestionSummary;
render(<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />);
expect(screen.getByTestId("formatted-headline")).toHaveTextContent("Recalled Headline");
// Look for text content with a more specific approach
const questionTypeElement = screen.getByText((content) => {
return content.includes("Open Text") && !content.includes("common.question_id");
});
expect(questionTypeElement).toBeInTheDocument();
// Check for responses text specifically
expect(
screen.getByText((content) => {
return content.includes("42") && content.includes("common.responses");
})
).toBeInTheDocument();
expect(screen.getByTestId("question-icon")).toBeInTheDocument();
expect(screen.getByTestId("settings-id")).toHaveTextContent("common.question_id: q1");
expect(screen.queryByText("environments.surveys.edit.optional")).not.toBeInTheDocument();
});
test("shows 'optional' tag when question is not required", () => {
const questionSummary = {
question: {
id: "q2",
headline: { default: "Optional Question" },
type: "multipleChoice" as TSurveyQuestionTypeEnum,
required: false,
},
responseCount: 10,
} as unknown as TSurveyQuestionSummary;
render(<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />);
expect(screen.getByText("environments.surveys.edit.optional")).toBeInTheDocument();
});
test("hides response count when showResponses is false", () => {
const questionSummary = {
question: {
id: "q3",
headline: { default: "No Response Count Question" },
type: "openText" as TSurveyQuestionTypeEnum,
required: true,
},
responseCount: 15,
} as unknown as TSurveyQuestionSummary;
render(<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} showResponses={false} />);
expect(
screen.queryByText((content) => content.includes("15") && content.includes("common.responses"))
).not.toBeInTheDocument();
});
test("shows unknown question type for unrecognized type", () => {
const questionSummary = {
question: {
id: "q4",
headline: { default: "Unknown Type Question" },
type: "unknownType" as TSurveyQuestionTypeEnum,
required: true,
},
responseCount: 5,
} as unknown as TSurveyQuestionSummary;
render(<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />);
// Look for text in the question type element specifically
const unknownTypeElement = screen.getByText((content) => {
return (
content.includes("environments.surveys.summary.unknown_question_type") &&
!content.includes("common.question_id")
);
});
expect(unknownTypeElement).toBeInTheDocument();
});
test("renders additional info when provided", () => {
const questionSummary = {
question: {
id: "q5",
headline: { default: "With Additional Info" },
type: "openText" as TSurveyQuestionTypeEnum,
required: true,
},
responseCount: 20,
} as unknown as TSurveyQuestionSummary;
const additionalInfo = <div data-testid="additional-info">Extra Information</div>;
render(
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
additionalInfo={additionalInfo}
/>
);
expect(screen.getByTestId("additional-info")).toBeInTheDocument();
expect(screen.getByText("Extra Information")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,104 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionSummaryRanking, TSurveyType } from "@formbricks/types/surveys/types";
import { RankingSummary } from "./RankingSummary";
// Mock dependencies
vi.mock("./QuestionSummaryHeader", () => ({
QuestionSummaryHeader: () => <div data-testid="question-summary-header" />,
}));
vi.mock("../lib/utils", () => ({
convertFloatToNDecimal: (value: number) => value.toFixed(2),
}));
describe("RankingSummary", () => {
afterEach(() => {
cleanup();
});
const survey = {} as TSurvey;
const surveyType: TSurveyType = "app";
test("renders ranking results in correct order", () => {
const questionSummary = {
question: { id: "q1", headline: "Rank the following" },
choices: {
option1: { value: "Option A", avgRanking: 1.5, others: [] },
option2: { value: "Option B", avgRanking: 2.3, others: [] },
option3: { value: "Option C", avgRanking: 1.2, others: [] },
},
} as unknown as TSurveyQuestionSummaryRanking;
render(<RankingSummary questionSummary={questionSummary} survey={survey} surveyType={surveyType} />);
expect(screen.getByTestId("question-summary-header")).toBeInTheDocument();
// Check order: should be sorted by avgRanking (ascending)
const options = screen.getAllByText(/Option [A-C]/);
expect(options[0]).toHaveTextContent("Option C"); // 1.2 (lowest avgRanking first)
expect(options[1]).toHaveTextContent("Option A"); // 1.5
expect(options[2]).toHaveTextContent("Option B"); // 2.3
// Check rankings are displayed
expect(screen.getByText("#1")).toBeInTheDocument();
expect(screen.getByText("#2")).toBeInTheDocument();
expect(screen.getByText("#3")).toBeInTheDocument();
// Check average values are displayed
expect(screen.getByText("#1.20")).toBeInTheDocument();
expect(screen.getByText("#1.50")).toBeInTheDocument();
expect(screen.getByText("#2.30")).toBeInTheDocument();
});
test("renders 'other values found' section when others exist", () => {
const questionSummary = {
question: { id: "q1", headline: "Rank the following" },
choices: {
option1: {
value: "Option A",
avgRanking: 1.0,
others: [{ value: "Other value", count: 2 }],
},
},
} as unknown as TSurveyQuestionSummaryRanking;
render(<RankingSummary questionSummary={questionSummary} survey={survey} surveyType={surveyType} />);
expect(screen.getByText("environments.surveys.summary.other_values_found")).toBeInTheDocument();
});
test("shows 'User' column in other values section for app survey type", () => {
const questionSummary = {
question: { id: "q1", headline: "Rank the following" },
choices: {
option1: {
value: "Option A",
avgRanking: 1.0,
others: [{ value: "Other value", count: 1 }],
},
},
} as unknown as TSurveyQuestionSummaryRanking;
render(<RankingSummary questionSummary={questionSummary} survey={survey} surveyType="app" />);
expect(screen.getByText("common.user")).toBeInTheDocument();
});
test("doesn't show 'User' column for link survey type", () => {
const questionSummary = {
question: { id: "q1", headline: "Rank the following" },
choices: {
option1: {
value: "Option A",
avgRanking: 1.0,
others: [{ value: "Other value", count: 1 }],
},
},
} as unknown as TSurveyQuestionSummaryRanking;
render(<RankingSummary questionSummary={questionSummary} survey={survey} surveyType="link" />);
expect(screen.queryByText("common.user")).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,125 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionTypeEnum, TSurveySummary } from "@formbricks/types/surveys/types";
import { SummaryDropOffs } from "./SummaryDropOffs";
// Mock dependencies
vi.mock("@/lib/utils/recall", () => ({
recallToHeadline: () => ({ default: "Recalled Question" }),
}));
vi.mock("@/modules/survey/editor/lib/utils", () => ({
formatTextWithSlashes: (text) => <span data-testid="formatted-text">{text}</span>,
}));
vi.mock("@/modules/survey/lib/questions", () => ({
getQuestionIcon: () => () => <div data-testid="question-icon" />,
}));
vi.mock("@/modules/ui/components/tooltip", () => ({
TooltipProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Tooltip: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
TooltipTrigger: ({ children }: { children: React.ReactNode }) => (
<div data-testid="tooltip-trigger">{children}</div>
),
TooltipContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="tooltip-content">{children}</div>
),
}));
vi.mock("lucide-react", () => ({
TimerIcon: () => <div data-testid="timer-icon" />,
}));
describe("SummaryDropOffs", () => {
afterEach(() => {
cleanup();
});
const mockSurvey = {} as TSurvey;
const mockDropOff: TSurveySummary["dropOff"] = [
{
questionId: "q1",
headline: "First Question",
questionType: TSurveyQuestionTypeEnum.OpenText,
ttc: 15000, // 15 seconds
impressions: 100,
dropOffCount: 20,
dropOffPercentage: 20,
},
{
questionId: "q2",
headline: "Second Question",
questionType: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
ttc: 30000, // 30 seconds
impressions: 80,
dropOffCount: 15,
dropOffPercentage: 18.75,
},
{
questionId: "q3",
headline: "Third Question",
questionType: TSurveyQuestionTypeEnum.Rating,
ttc: 0, // No time data
impressions: 65,
dropOffCount: 10,
dropOffPercentage: 15.38,
},
];
test("renders header row with correct columns", () => {
render(<SummaryDropOffs dropOff={mockDropOff} survey={mockSurvey} />);
// Check header
expect(screen.getByText("common.questions")).toBeInTheDocument();
expect(screen.getByTestId("tooltip-trigger")).toBeInTheDocument();
expect(screen.getByTestId("timer-icon")).toBeInTheDocument();
expect(screen.getByText("environments.surveys.summary.impressions")).toBeInTheDocument();
expect(screen.getByText("environments.surveys.summary.drop_offs")).toBeInTheDocument();
});
test("renders tooltip with correct content", () => {
render(<SummaryDropOffs dropOff={mockDropOff} survey={mockSurvey} />);
expect(screen.getByTestId("tooltip-content")).toBeInTheDocument();
expect(screen.getByText("environments.surveys.summary.ttc_tooltip")).toBeInTheDocument();
});
test("renders all drop-off items with correct data", () => {
render(<SummaryDropOffs dropOff={mockDropOff} survey={mockSurvey} />);
// There should be 3 rows of data (one for each question)
expect(screen.getAllByTestId("question-icon")).toHaveLength(3);
expect(screen.getAllByTestId("formatted-text")).toHaveLength(3);
// Check time to complete values
expect(screen.getByText("15.00s")).toBeInTheDocument(); // 15000ms converted to seconds
expect(screen.getByText("30.00s")).toBeInTheDocument(); // 30000ms converted to seconds
expect(screen.getByText("N/A")).toBeInTheDocument(); // 0ms shown as N/A
// Check impressions values
expect(screen.getByText("100")).toBeInTheDocument();
expect(screen.getByText("80")).toBeInTheDocument();
expect(screen.getByText("65")).toBeInTheDocument();
// Check drop-off counts and percentages
expect(screen.getByText("20")).toBeInTheDocument();
expect(screen.getByText("(20%)")).toBeInTheDocument();
expect(screen.getByText("15")).toBeInTheDocument();
expect(screen.getByText("(19%)")).toBeInTheDocument(); // 18.75% rounded to 19%
expect(screen.getByText("10")).toBeInTheDocument();
expect(screen.getByText("(15%)")).toBeInTheDocument(); // 15.38% rounded to 15%
});
test("renders empty state when dropOff array is empty", () => {
render(<SummaryDropOffs dropOff={[]} survey={mockSurvey} />);
// Header should still be visible
expect(screen.getByText("common.questions")).toBeInTheDocument();
// But no question icons
expect(screen.queryByTestId("question-icon")).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,229 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { SummaryPage } from "./SummaryPage";
// Mock actions
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions", () => ({
getResponseCountAction: vi.fn().mockResolvedValue({ data: 42 }),
getSurveySummaryAction: vi.fn().mockResolvedValue({
data: {
meta: {
completedPercentage: 80,
completedResponses: 40,
displayCount: 50,
dropOffPercentage: 20,
dropOffCount: 10,
startsPercentage: 100,
totalResponses: 50,
ttcAverage: 120,
},
dropOff: [
{
questionId: "q1",
headline: "Question 1",
questionType: "openText",
ttc: 20000,
impressions: 50,
dropOffCount: 5,
dropOffPercentage: 10,
},
],
summary: [
{
question: { id: "q1", headline: "Question 1", type: "openText", required: true },
responseCount: 45,
type: "openText",
samples: [],
},
],
},
}),
}));
vi.mock("@/app/share/[sharingKey]/actions", () => ({
getResponseCountBySurveySharingKeyAction: vi.fn().mockResolvedValue({ data: 42 }),
getSummaryBySurveySharingKeyAction: vi.fn().mockResolvedValue({
data: {
meta: {
completedPercentage: 80,
completedResponses: 40,
displayCount: 50,
dropOffPercentage: 20,
dropOffCount: 10,
startsPercentage: 100,
totalResponses: 50,
ttcAverage: 120,
},
dropOff: [
{
questionId: "q1",
headline: "Question 1",
questionType: "openText",
ttc: 20000,
impressions: 50,
dropOffCount: 5,
dropOffPercentage: 10,
},
],
summary: [
{
question: { id: "q1", headline: "Question 1", type: "openText", required: true },
responseCount: 45,
type: "openText",
samples: [],
},
],
},
}),
}));
// Mock components
vi.mock(
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs",
() => ({
SummaryDropOffs: () => <div data-testid="summary-drop-offs">DropOffs Component</div>,
})
);
vi.mock(
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList",
() => ({
SummaryList: ({ summary, responseCount }: any) => (
<div data-testid="summary-list">
<span>Response Count: {responseCount}</span>
<span>Summary Items: {summary.length}</span>
</div>
),
})
);
vi.mock(
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata",
() => ({
SummaryMetadata: ({ showDropOffs, setShowDropOffs, isLoading }: any) => (
<div data-testid="summary-metadata">
<span>Is Loading: {isLoading ? "true" : "false"}</span>
<button onClick={() => setShowDropOffs(!showDropOffs)}>Toggle Dropoffs</button>
</div>
),
})
);
vi.mock(
"@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop",
() => ({
__esModule: true,
default: () => <div data-testid="scroll-to-top">Scroll To Top</div>,
})
);
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter", () => ({
CustomFilter: () => <div data-testid="custom-filter">Custom Filter</div>,
}));
vi.mock("@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResultsShareButton", () => ({
ResultsShareButton: () => <div data-testid="results-share-button">Share Results</div>,
}));
// Mock context
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
useResponseFilter: () => ({
selectedFilter: { filter: [], onlyComplete: false },
dateRange: { from: null, to: null },
resetState: vi.fn(),
}),
}));
// Mock hooks
vi.mock("@/lib/utils/hooks/useIntervalWhenFocused", () => ({
useIntervalWhenFocused: vi.fn(),
}));
vi.mock("@/lib/utils/recall", () => ({
replaceHeadlineRecall: (survey: any) => survey,
}));
vi.mock("next/navigation", () => ({
useParams: () => ({}),
useSearchParams: () => ({ get: () => null }),
}));
describe("SummaryPage", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const mockEnvironment = { id: "env-123" } as TEnvironment;
const mockSurvey = {
id: "survey-123",
environmentId: "env-123",
} as TSurvey;
const locale = "en-US" as TUserLocale;
const defaultProps = {
environment: mockEnvironment,
survey: mockSurvey,
surveyId: "survey-123",
webAppUrl: "https://app.example.com",
totalResponseCount: 50,
isAIEnabled: true,
locale,
isReadOnly: false,
};
test("renders loading state initially", () => {
render(<SummaryPage {...defaultProps} />);
expect(screen.getByTestId("summary-metadata")).toBeInTheDocument();
expect(screen.getByText("Is Loading: true")).toBeInTheDocument();
});
test("renders summary components after loading", async () => {
render(<SummaryPage {...defaultProps} />);
// Wait for loading to complete
await waitFor(() => {
expect(screen.getByText("Is Loading: false")).toBeInTheDocument();
});
expect(screen.getByTestId("custom-filter")).toBeInTheDocument();
expect(screen.getByTestId("results-share-button")).toBeInTheDocument();
expect(screen.getByTestId("scroll-to-top")).toBeInTheDocument();
expect(screen.getByTestId("summary-list")).toBeInTheDocument();
});
test("shows drop-offs component when toggled", async () => {
const user = userEvent.setup();
render(<SummaryPage {...defaultProps} />);
// Wait for loading to complete
await waitFor(() => {
expect(screen.getByText("Is Loading: false")).toBeInTheDocument();
});
// Drop-offs should initially be hidden
expect(screen.queryByTestId("summary-drop-offs")).not.toBeInTheDocument();
// Toggle drop-offs
await user.click(screen.getByText("Toggle Dropoffs"));
// Drop-offs should now be visible
expect(screen.getByTestId("summary-drop-offs")).toBeInTheDocument();
});
test("doesn't show share button in read-only mode", async () => {
render(<SummaryPage {...defaultProps} isReadOnly={true} />);
// Wait for loading to complete
await waitFor(() => {
expect(screen.getByText("Is Loading: false")).toBeInTheDocument();
});
expect(screen.queryByTestId("results-share-button")).not.toBeInTheDocument();
});
});

View File

@@ -47,7 +47,6 @@ export default defineConfig({
"app/layout.tsx",
"app/intercom/*.tsx",
"app/sentry/*.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/SurveyAnalysisCTA.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/ConsentSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/MatrixQuestionSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/MultipleChoiceSummary.tsx",
@@ -55,6 +54,19 @@ export default defineConfig({
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/PictureChoiceSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/RatingSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/SummaryMetadata.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/FileUploadSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/AddressSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/CalSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/ContactInfoSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/CTASummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/DateQuestionSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/EnableInsightsBanner.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/SummaryDropOffs.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/SummaryPage.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/RankingSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/OpenTextSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/HiddenFieldsSummary.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/QuestionSummaryHeader.tsx",
"app/(app)/environments/**/surveys/**/components/QuestionFilterComboBox.tsx",
"app/(app)/environments/**/surveys/**/components/QuestionsComboBox.tsx",
"app/(app)/environments/**/integrations/airtable/components/ManageIntegration.tsx",