mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 00:49:42 -06:00
chore: Added the tests to file upload summary (#5504)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user