mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 16:16:21 -06:00
feat: surface option ids (#6339)
This commit is contained in:
committed by
GitHub
parent
3b07a6d013
commit
287c45f996
@@ -94,7 +94,7 @@ const getQuestionColumnsData = (
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{choiceIds.map((choiceId, index) => (
|
||||
<IdBadge key={choiceId || index} id={choiceId} />
|
||||
<IdBadge key={`${choiceId}-${index}`} id={choiceId} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -195,6 +195,7 @@ const getQuestionColumnsData = (
|
||||
responseData={responseValue}
|
||||
language={language}
|
||||
isExpanded={isExpanded}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -246,6 +247,7 @@ const getQuestionColumnsData = (
|
||||
responseData={responseValue}
|
||||
language={language}
|
||||
isExpanded={isExpanded}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -312,7 +314,7 @@ export const generateResponseTableColumns = (
|
||||
header: t("common.status"),
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
return <ResponseBadges items={[status]} />;
|
||||
return <ResponseBadges items={[{ value: status }]} showId={false} />;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -325,9 +327,10 @@ export const generateResponseTableColumns = (
|
||||
const tagsArray = tags.map((tag) => tag.name);
|
||||
return (
|
||||
<ResponseBadges
|
||||
items={tagsArray}
|
||||
items={tagsArray.map((tag) => ({ value: tag }))}
|
||||
isExpanded={isExpanded}
|
||||
icon={<TagIcon className="h-4 w-4 text-slate-500" />}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@ vi.mock("@/modules/ui/components/avatars", () => ({
|
||||
PersonAvatar: ({ personId }: any) => <div data-testid="avatar">{personId}</div>,
|
||||
}));
|
||||
vi.mock("./QuestionSummaryHeader", () => ({ QuestionSummaryHeader: () => <div data-testid="header" /> }));
|
||||
vi.mock("@/modules/ui/components/id-badge", () => ({
|
||||
IdBadge: ({ id }: { id: string }) => (
|
||||
<div data-testid="id-badge" data-id={id}>
|
||||
ID: {id}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("MultipleChoiceSummary", () => {
|
||||
afterEach(() => {
|
||||
@@ -160,8 +167,8 @@ describe("MultipleChoiceSummary", () => {
|
||||
/>
|
||||
);
|
||||
const btns = screen.getAllByRole("button");
|
||||
expect(btns[0]).toHaveTextContent("2 - Y50%2 common.selections");
|
||||
expect(btns[1]).toHaveTextContent("1 - X50%1 common.selection");
|
||||
expect(btns[0]).toHaveTextContent("2 - YID: other2 common.selections50%");
|
||||
expect(btns[1]).toHaveTextContent("1 - XID: other1 common.selection50%");
|
||||
});
|
||||
|
||||
test("places choice with others after one without when reversed inputs", () => {
|
||||
@@ -272,4 +279,127 @@ describe("MultipleChoiceSummary", () => {
|
||||
["O5"]
|
||||
);
|
||||
});
|
||||
|
||||
// New tests for IdBadge functionality
|
||||
test("renders IdBadge when choice ID is found", () => {
|
||||
const setFilter = vi.fn();
|
||||
const q = {
|
||||
question: {
|
||||
id: "q6",
|
||||
headline: "H6",
|
||||
type: "multipleChoiceSingle",
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option A" } },
|
||||
{ id: "choice2", label: { default: "Option B" } },
|
||||
],
|
||||
},
|
||||
choices: {
|
||||
"Option A": { value: "Option A", count: 5, percentage: 50, others: [] },
|
||||
"Option B": { value: "Option B", count: 5, percentage: 50, others: [] },
|
||||
},
|
||||
type: "multipleChoiceSingle",
|
||||
selectionCount: 0,
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<MultipleChoiceSummary
|
||||
questionSummary={q}
|
||||
environmentId="env"
|
||||
surveyType="link"
|
||||
survey={baseSurvey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "choice1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "choice2");
|
||||
expect(idBadges[0]).toHaveTextContent("ID: choice1");
|
||||
expect(idBadges[1]).toHaveTextContent("ID: choice2");
|
||||
});
|
||||
|
||||
test("getChoiceIdByValue function correctly maps values to IDs", () => {
|
||||
const setFilter = vi.fn();
|
||||
const q = {
|
||||
question: {
|
||||
id: "q8",
|
||||
headline: "H8",
|
||||
type: "multipleChoiceMulti",
|
||||
choices: [
|
||||
{ id: "id-apple", label: { default: "Apple" } },
|
||||
{ id: "id-banana", label: { default: "Banana" } },
|
||||
{ id: "id-cherry", label: { default: "Cherry" } },
|
||||
],
|
||||
},
|
||||
choices: {
|
||||
Apple: { value: "Apple", count: 3, percentage: 30, others: [] },
|
||||
Banana: { value: "Banana", count: 4, percentage: 40, others: [] },
|
||||
Cherry: { value: "Cherry", count: 3, percentage: 30, others: [] },
|
||||
},
|
||||
type: "multipleChoiceMulti",
|
||||
selectionCount: 0,
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<MultipleChoiceSummary
|
||||
questionSummary={q}
|
||||
environmentId="env"
|
||||
surveyType="link"
|
||||
survey={baseSurvey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(3);
|
||||
|
||||
// Check that each badge has the correct ID
|
||||
const expectedMappings = [
|
||||
{ text: "Banana", id: "id-banana" }, // Highest count appears first
|
||||
{ text: "Apple", id: "id-apple" },
|
||||
{ text: "Cherry", id: "id-cherry" },
|
||||
];
|
||||
|
||||
expectedMappings.forEach(({ text, id }, index) => {
|
||||
expect(screen.getByText(`${3 - index} - ${text}`)).toBeInTheDocument();
|
||||
expect(idBadges[index]).toHaveAttribute("data-id", id);
|
||||
});
|
||||
});
|
||||
|
||||
test("handles choices with special characters in labels", () => {
|
||||
const setFilter = vi.fn();
|
||||
const q = {
|
||||
question: {
|
||||
id: "q9",
|
||||
headline: "H9",
|
||||
type: "multipleChoiceSingle",
|
||||
choices: [
|
||||
{ id: "special-1", label: { default: "Option & Choice" } },
|
||||
{ id: "special-2", label: { default: "Choice with 'quotes'" } },
|
||||
],
|
||||
},
|
||||
choices: {
|
||||
"Option & Choice": { value: "Option & Choice", count: 2, percentage: 50, others: [] },
|
||||
"Choice with 'quotes'": { value: "Choice with 'quotes'", count: 2, percentage: 50, others: [] },
|
||||
},
|
||||
type: "multipleChoiceSingle",
|
||||
selectionCount: 0,
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<MultipleChoiceSummary
|
||||
questionSummary={q}
|
||||
environmentId="env"
|
||||
surveyType="link"
|
||||
survey={baseSurvey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "special-1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "special-2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
@@ -84,90 +86,95 @@ export const MultipleChoiceSummary = ({
|
||||
}
|
||||
/>
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result, resultsIdx) => (
|
||||
<Fragment key={result.value}>
|
||||
<button
|
||||
className="group w-full cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
questionSummary.type === "multipleChoiceSingle" || otherValue === result.value
|
||||
? t("environments.surveys.summary.includes_either")
|
||||
: t("environments.surveys.summary.includes_all"),
|
||||
[result.value]
|
||||
)
|
||||
}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal">
|
||||
<p className="font-semibold text-slate-700 underline-offset-4 group-hover:underline">
|
||||
{results.length - resultsIdx} - {result.value}
|
||||
</p>
|
||||
<div>
|
||||
{results.map((result, resultsIdx) => {
|
||||
const choiceId = getChoiceIdByValue(result.value, questionSummary.question);
|
||||
return (
|
||||
<Fragment key={result.value}>
|
||||
<button
|
||||
type="button"
|
||||
className="group w-full cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
questionSummary.type === "multipleChoiceSingle" || otherValue === result.value
|
||||
? t("environments.surveys.summary.includes_either")
|
||||
: t("environments.surveys.summary.includes_all"),
|
||||
[result.value]
|
||||
)
|
||||
}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-2 sm:justify-normal">
|
||||
<p className="font-semibold text-slate-700 underline-offset-4 group-hover:underline">
|
||||
{results.length - resultsIdx} - {result.value}
|
||||
</p>
|
||||
{choiceId && <IdBadge id={choiceId} />}
|
||||
</div>
|
||||
<div className="flex w-full space-x-2">
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
|
||||
</p>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="group-hover:opacity-80">
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
|
||||
</div>
|
||||
</button>
|
||||
{result.others && result.others.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6">
|
||||
{t("environments.surveys.summary.other_values_found")}
|
||||
</div>
|
||||
<div className="col-span-1 pl-6">{surveyType === "app" && t("common.user")}</div>
|
||||
<div className="group-hover:opacity-80">
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
|
||||
</div>
|
||||
{result.others
|
||||
.filter((otherValue) => otherValue.value !== "")
|
||||
.slice(0, visibleOtherResponses)
|
||||
.map((otherValue, idx) => (
|
||||
<div key={`${idx}-${otherValue}`} dir="auto">
|
||||
{surveyType === "link" && (
|
||||
<div className="ph-no-capture col-span-1 m-2 flex h-10 items-center rounded-lg pl-4 text-sm font-medium text-slate-900">
|
||||
<span>{otherValue.value}</span>
|
||||
</div>
|
||||
)}
|
||||
{surveyType === "app" && otherValue.contact && (
|
||||
<Link
|
||||
href={
|
||||
otherValue.contact.id
|
||||
? `/environments/${environmentId}/contacts/${otherValue.contact.id}`
|
||||
: { pathname: null }
|
||||
}
|
||||
className="m-2 grid h-16 grid-cols-2 items-center rounded-lg text-sm hover:bg-slate-100">
|
||||
<div className="ph-no-capture col-span-1 pl-4 font-medium text-slate-900">
|
||||
</button>
|
||||
{result.others && result.others.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6">
|
||||
{t("environments.surveys.summary.other_values_found")}
|
||||
</div>
|
||||
<div className="col-span-1 pl-6">{surveyType === "app" && t("common.user")}</div>
|
||||
</div>
|
||||
{result.others
|
||||
.filter((otherValue) => otherValue.value !== "")
|
||||
.slice(0, visibleOtherResponses)
|
||||
.map((otherValue, idx) => (
|
||||
<div key={`${idx}-${otherValue}`} dir="auto">
|
||||
{surveyType === "link" && (
|
||||
<div className="ph-no-capture col-span-1 m-2 flex h-10 items-center rounded-lg pl-4 text-sm font-medium text-slate-900">
|
||||
<span>{otherValue.value}</span>
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
|
||||
{otherValue.contact.id && <PersonAvatar personId={otherValue.contact.id} />}
|
||||
<span>
|
||||
{getContactIdentifier(otherValue.contact, otherValue.contactAttributes)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
)}
|
||||
{surveyType === "app" && otherValue.contact && (
|
||||
<Link
|
||||
href={
|
||||
otherValue.contact.id
|
||||
? `/environments/${environmentId}/contacts/${otherValue.contact.id}`
|
||||
: { pathname: null }
|
||||
}
|
||||
className="m-2 grid h-16 grid-cols-2 items-center rounded-lg text-sm hover:bg-slate-100">
|
||||
<div className="ph-no-capture col-span-1 pl-4 font-medium text-slate-900">
|
||||
<span>{otherValue.value}</span>
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
|
||||
{otherValue.contact.id && <PersonAvatar personId={otherValue.contact.id} />}
|
||||
<span>
|
||||
{getContactIdentifier(otherValue.contact, otherValue.contactAttributes)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{visibleOtherResponses < result.others.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{visibleOtherResponses < result.others.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
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, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { PictureChoiceSummary } from "./PictureChoiceSummary";
|
||||
|
||||
vi.mock("@/modules/ui/components/progress-bar", () => ({
|
||||
@@ -12,6 +16,19 @@ vi.mock("@/modules/ui/components/progress-bar", () => ({
|
||||
vi.mock("./QuestionSummaryHeader", () => ({
|
||||
QuestionSummaryHeader: ({ additionalInfo }: any) => <div data-testid="header">{additionalInfo}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/id-badge", () => ({
|
||||
IdBadge: ({ id }: { id: string }) => (
|
||||
<div data-testid="id-badge" data-id={id}>
|
||||
ID: {id}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/response/utils", () => ({
|
||||
getChoiceIdByValue: (value: string, question: TSurveyPictureSelectionQuestion) => {
|
||||
return question.choices?.find((choice) => choice.imageUrl === value)?.id ?? "other";
|
||||
},
|
||||
}));
|
||||
|
||||
// mock next image
|
||||
vi.mock("next/image", () => ({
|
||||
@@ -88,4 +105,73 @@ describe("PictureChoiceSummary", () => {
|
||||
|
||||
expect(screen.getByTestId("header")).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
// New tests for IdBadge functionality
|
||||
test("renders IdBadge when choice ID is found via imageUrl", () => {
|
||||
const choices = [
|
||||
{ id: "choice1", imageUrl: "https://example.com/img1.png", percentage: 50, count: 5 },
|
||||
{ id: "choice2", imageUrl: "https://example.com/img2.png", percentage: 50, count: 5 },
|
||||
];
|
||||
const questionSummary = {
|
||||
choices,
|
||||
question: {
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
headline: "Picture Question",
|
||||
allowMulti: true,
|
||||
choices: [
|
||||
{ id: "pic-choice-1", imageUrl: "https://example.com/img1.png" },
|
||||
{ id: "pic-choice-2", imageUrl: "https://example.com/img2.png" },
|
||||
],
|
||||
},
|
||||
selectionCount: 10,
|
||||
} as any;
|
||||
|
||||
render(<PictureChoiceSummary questionSummary={questionSummary} survey={survey} setFilter={() => {}} />);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "pic-choice-1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "pic-choice-2");
|
||||
expect(idBadges[0]).toHaveTextContent("ID: pic-choice-1");
|
||||
expect(idBadges[1]).toHaveTextContent("ID: pic-choice-2");
|
||||
});
|
||||
|
||||
test("getChoiceIdByValue function correctly maps imageUrl to choice ID", () => {
|
||||
const choices = [
|
||||
{ id: "choice1", imageUrl: "https://cdn.example.com/photo1.jpg", percentage: 33.33, count: 2 },
|
||||
{ id: "choice2", imageUrl: "https://cdn.example.com/photo2.jpg", percentage: 33.33, count: 2 },
|
||||
{ id: "choice3", imageUrl: "https://cdn.example.com/photo3.jpg", percentage: 33.33, count: 2 },
|
||||
];
|
||||
const questionSummary = {
|
||||
choices,
|
||||
question: {
|
||||
id: "q4",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
headline: "Photo Selection",
|
||||
allowMulti: true,
|
||||
choices: [
|
||||
{ id: "photo-a", imageUrl: "https://cdn.example.com/photo1.jpg" },
|
||||
{ id: "photo-b", imageUrl: "https://cdn.example.com/photo2.jpg" },
|
||||
{ id: "photo-c", imageUrl: "https://cdn.example.com/photo3.jpg" },
|
||||
],
|
||||
},
|
||||
selectionCount: 6,
|
||||
} as any;
|
||||
|
||||
render(<PictureChoiceSummary questionSummary={questionSummary} survey={survey} setFilter={() => {}} />);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(3);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "photo-a");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "photo-b");
|
||||
expect(idBadges[2]).toHaveAttribute("data-id", "photo-c");
|
||||
|
||||
// Verify the images are also rendered correctly
|
||||
const images = screen.getAllByRole("img");
|
||||
expect(images).toHaveLength(3);
|
||||
expect(images[0]).toHaveAttribute("src", "https://cdn.example.com/photo1.jpg");
|
||||
expect(images[1]).toHaveAttribute("src", "https://cdn.example.com/photo2.jpg");
|
||||
expect(images[2]).toHaveAttribute("src", "https://cdn.example.com/photo3.jpg");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
@@ -29,6 +31,7 @@ interface PictureChoiceSummaryProps {
|
||||
export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: PictureChoiceSummaryProps) => {
|
||||
const results = questionSummary.choices;
|
||||
const { t } = useTranslate();
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
@@ -44,43 +47,48 @@ export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: Pic
|
||||
}
|
||||
/>
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result, index) => (
|
||||
<button
|
||||
className="w-full cursor-pointer hover:opacity-80"
|
||||
key={result.id}
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
t("environments.surveys.summary.includes_all"),
|
||||
[`${t("environments.surveys.edit.picture_idx", { idx: index + 1 })}`]
|
||||
)
|
||||
}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal">
|
||||
<div className="relative h-32 w-[220px]">
|
||||
<Image
|
||||
src={result.imageUrl}
|
||||
alt="choice-image"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-md"
|
||||
/>
|
||||
{results.map((result, index) => {
|
||||
const choiceId = getChoiceIdByValue(result.imageUrl, questionSummary.question);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full cursor-pointer hover:opacity-80"
|
||||
key={result.id}
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
t("environments.surveys.summary.includes_all"),
|
||||
[`${t("environments.surveys.edit.picture_idx", { idx: index + 1 })}`]
|
||||
)
|
||||
}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-2 sm:justify-normal">
|
||||
<div className="relative h-32 w-[220px]">
|
||||
<Image
|
||||
src={result.imageUrl}
|
||||
alt="choice-image"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<div className="self-end">{choiceId && <IdBadge id={choiceId} />}</div>
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
<div className="flex w-full space-x-2">
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
|
||||
</p>
|
||||
<p className="self-end rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100 || 0} />
|
||||
</button>
|
||||
))}
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100 || 0} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { TSurvey, TSurveyQuestionSummaryRanking } from "@formbricks/types/surveys/types";
|
||||
import { RankingSummary } from "./RankingSummary";
|
||||
|
||||
// Mock dependencies
|
||||
@@ -12,17 +12,32 @@ vi.mock("../lib/utils", () => ({
|
||||
convertFloatToNDecimal: (value: number) => value.toFixed(2),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/id-badge", () => ({
|
||||
IdBadge: ({ id }: { id: string }) => (
|
||||
<div data-testid="id-badge" data-id={id}>
|
||||
ID: {id}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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" },
|
||||
question: {
|
||||
id: "q1",
|
||||
headline: "Rank the following",
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option A" } },
|
||||
{ id: "choice2", label: { default: "Option B" } },
|
||||
{ id: "choice3", label: { default: "Option C" } },
|
||||
],
|
||||
},
|
||||
choices: {
|
||||
option1: { value: "Option A", avgRanking: 1.5, others: [] },
|
||||
option2: { value: "Option B", avgRanking: 2.3, others: [] },
|
||||
@@ -30,7 +45,7 @@ describe("RankingSummary", () => {
|
||||
},
|
||||
} as unknown as TSurveyQuestionSummaryRanking;
|
||||
|
||||
render(<RankingSummary questionSummary={questionSummary} survey={survey} surveyType={surveyType} />);
|
||||
render(<RankingSummary questionSummary={questionSummary} survey={survey} />);
|
||||
|
||||
expect(screen.getByTestId("question-summary-header")).toBeInTheDocument();
|
||||
|
||||
@@ -51,43 +66,13 @@ describe("RankingSummary", () => {
|
||||
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" },
|
||||
question: {
|
||||
id: "q1",
|
||||
headline: "Rank the following",
|
||||
choices: [{ id: "choice1", label: { default: "Option A" } }],
|
||||
},
|
||||
choices: {
|
||||
option1: {
|
||||
value: "Option A",
|
||||
@@ -97,8 +82,132 @@ describe("RankingSummary", () => {
|
||||
},
|
||||
} as unknown as TSurveyQuestionSummaryRanking;
|
||||
|
||||
render(<RankingSummary questionSummary={questionSummary} survey={survey} surveyType="link" />);
|
||||
render(<RankingSummary questionSummary={questionSummary} survey={survey} />);
|
||||
|
||||
expect(screen.queryByText("common.user")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// New tests for IdBadge functionality
|
||||
test("renders IdBadge when choice ID is found via label", () => {
|
||||
const questionSummary = {
|
||||
question: {
|
||||
id: "q2",
|
||||
headline: "Rank these options",
|
||||
choices: [
|
||||
{ id: "rank-choice-1", label: { default: "First Option" } },
|
||||
{ id: "rank-choice-2", label: { default: "Second Option" } },
|
||||
{ id: "rank-choice-3", label: { default: "Third Option" } },
|
||||
],
|
||||
},
|
||||
choices: {
|
||||
option1: { value: "First Option", avgRanking: 1.5, others: [] },
|
||||
option2: { value: "Second Option", avgRanking: 2.1, others: [] },
|
||||
option3: { value: "Third Option", avgRanking: 2.8, others: [] },
|
||||
},
|
||||
} as unknown as TSurveyQuestionSummaryRanking;
|
||||
|
||||
render(<RankingSummary questionSummary={questionSummary} survey={survey} />);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(3);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "rank-choice-1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "rank-choice-2");
|
||||
expect(idBadges[2]).toHaveAttribute("data-id", "rank-choice-3");
|
||||
expect(idBadges[0]).toHaveTextContent("ID: rank-choice-1");
|
||||
expect(idBadges[1]).toHaveTextContent("ID: rank-choice-2");
|
||||
expect(idBadges[2]).toHaveTextContent("ID: rank-choice-3");
|
||||
});
|
||||
|
||||
test("getChoiceIdByValue function correctly maps ranking values to choice IDs", () => {
|
||||
const questionSummary = {
|
||||
question: {
|
||||
id: "q4",
|
||||
headline: "Rate importance",
|
||||
choices: [
|
||||
{ id: "importance-high", label: { default: "Very Important" } },
|
||||
{ id: "importance-medium", label: { default: "Somewhat Important" } },
|
||||
{ id: "importance-low", label: { default: "Not Important" } },
|
||||
],
|
||||
},
|
||||
choices: {
|
||||
option1: { value: "Very Important", avgRanking: 1.2, others: [] },
|
||||
option2: { value: "Somewhat Important", avgRanking: 2.0, others: [] },
|
||||
option3: { value: "Not Important", avgRanking: 2.8, others: [] },
|
||||
},
|
||||
} as unknown as TSurveyQuestionSummaryRanking;
|
||||
|
||||
render(<RankingSummary questionSummary={questionSummary} survey={survey} />);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(3);
|
||||
|
||||
// Should be ordered by avgRanking (ascending)
|
||||
expect(screen.getByText("Very Important")).toBeInTheDocument(); // avgRanking: 1.2
|
||||
expect(screen.getByText("Somewhat Important")).toBeInTheDocument(); // avgRanking: 2.0
|
||||
expect(screen.getByText("Not Important")).toBeInTheDocument(); // avgRanking: 2.8
|
||||
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "importance-high");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "importance-medium");
|
||||
expect(idBadges[2]).toHaveAttribute("data-id", "importance-low");
|
||||
});
|
||||
|
||||
test("handles mixed choices with and without matching IDs", () => {
|
||||
const questionSummary = {
|
||||
question: {
|
||||
id: "q5",
|
||||
headline: "Mixed options",
|
||||
choices: [
|
||||
{ id: "valid-choice-1", label: { default: "Valid Option" } },
|
||||
{ id: "valid-choice-2", label: { default: "Another Valid Option" } },
|
||||
],
|
||||
},
|
||||
choices: {
|
||||
option1: { value: "Valid Option", avgRanking: 1.5, others: [] },
|
||||
option2: { value: "Unknown Option", avgRanking: 2.0, others: [] },
|
||||
option3: { value: "Another Valid Option", avgRanking: 2.5, others: [] },
|
||||
},
|
||||
} as unknown as TSurveyQuestionSummaryRanking;
|
||||
|
||||
render(<RankingSummary questionSummary={questionSummary} survey={survey} />);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(3); // Only 2 out of 3 should have badges
|
||||
|
||||
// Check that all options are still displayed
|
||||
expect(screen.getByText("Valid Option")).toBeInTheDocument();
|
||||
expect(screen.getByText("Unknown Option")).toBeInTheDocument();
|
||||
expect(screen.getByText("Another Valid Option")).toBeInTheDocument();
|
||||
|
||||
// Check that only the valid choices have badges
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "valid-choice-1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "other");
|
||||
expect(idBadges[2]).toHaveAttribute("data-id", "valid-choice-2");
|
||||
});
|
||||
|
||||
test("handles special characters in choice labels", () => {
|
||||
const questionSummary = {
|
||||
question: {
|
||||
id: "q6",
|
||||
headline: "Special characters test",
|
||||
choices: [
|
||||
{ id: "special-1", label: { default: "Option with 'quotes'" } },
|
||||
{ id: "special-2", label: { default: "Option & Ampersand" } },
|
||||
],
|
||||
},
|
||||
choices: {
|
||||
option1: { value: "Option with 'quotes'", avgRanking: 1.0, others: [] },
|
||||
option2: { value: "Option & Ampersand", avgRanking: 2.0, others: [] },
|
||||
},
|
||||
} as unknown as TSurveyQuestionSummaryRanking;
|
||||
|
||||
render(<RankingSummary questionSummary={questionSummary} survey={survey} />);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "special-1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "special-2");
|
||||
|
||||
expect(screen.getByText("Option with 'quotes'")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option & Ampersand")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { TSurvey, TSurveyQuestionSummaryRanking, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestionSummaryRanking } from "@formbricks/types/surveys/types";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
|
||||
interface RankingSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryRanking;
|
||||
surveyType: TSurveyType;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const RankingSummary = ({ questionSummary, surveyType, survey }: RankingSummaryProps) => {
|
||||
export const RankingSummary = ({ questionSummary, survey }: RankingSummaryProps) => {
|
||||
// sort by count and transform to array
|
||||
const { t } = useTranslate();
|
||||
const results = Object.values(questionSummary.choices).sort((a, b) => {
|
||||
@@ -20,35 +21,30 @@ export const RankingSummary = ({ questionSummary, surveyType, survey }: RankingS
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result, resultsIdx) => (
|
||||
<div key={result.value} className="group cursor-pointer">
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal">
|
||||
<div className="flex w-full items-center">
|
||||
<span className="mr-2 text-slate-400">#{resultsIdx + 1}</span>
|
||||
<div className="rounded bg-slate-100 px-2 py-1">{result.value}</div>
|
||||
<span className="ml-auto flex items-center space-x-1">
|
||||
<span className="font-bold text-slate-600">
|
||||
#{convertFloatToNDecimal(result.avgRanking, 2)}
|
||||
{results.map((result, resultsIdx) => {
|
||||
const choiceId = getChoiceIdByValue(result.value, questionSummary.question);
|
||||
return (
|
||||
<div key={result.value} className="group cursor-pointer">
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-2 sm:justify-normal">
|
||||
<div className="flex w-full items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="mr-2 text-slate-400">#{resultsIdx + 1}</span>
|
||||
<div className="rounded bg-slate-100 px-2 py-1">{result.value}</div>
|
||||
{choiceId && <IdBadge id={choiceId} />}
|
||||
</div>
|
||||
<span className="ml-auto flex items-center space-x-1">
|
||||
<span className="font-bold text-slate-600">
|
||||
#{convertFloatToNDecimal(result.avgRanking, 2)}
|
||||
</span>
|
||||
<span>{t("environments.surveys.summary.average")}</span>
|
||||
</span>
|
||||
<span>{t("environments.surveys.summary.average")}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{result.others && result.others.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6">
|
||||
{t("environments.surveys.summary.other_values_found")}
|
||||
</div>
|
||||
<div className="col-span-1 pl-6">{surveyType === "app" && t("common.user")}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -244,7 +244,6 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
<RankingSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
surveyType={survey.type}
|
||||
survey={survey}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
TSurveyContactAttributes,
|
||||
TSurveyMetaFieldFilter,
|
||||
} from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyMultipleChoiceQuestion,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestion,
|
||||
TSurveyRankingQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { processResponseData } from "../responses";
|
||||
import { getTodaysDateTimeFormatted } from "../time";
|
||||
import { getFormattedDateTimeString } from "../utils/datetime";
|
||||
@@ -81,6 +87,17 @@ export const extractChoiceIdsFromResponse = (
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getChoiceIdByValue = (
|
||||
value: string,
|
||||
question: TSurveyMultipleChoiceQuestion | TSurveyRankingQuestion | TSurveyPictureSelectionQuestion
|
||||
) => {
|
||||
if (question.type === "pictureSelection") {
|
||||
return question.choices.find((choice) => choice.imageUrl === value)?.id ?? "other";
|
||||
}
|
||||
|
||||
return question.choices.find((choice) => choice.label.default === value)?.id ?? "other";
|
||||
};
|
||||
|
||||
export const calculateTtcTotal = (ttc: TResponseTtc) => {
|
||||
const result = { ...ttc };
|
||||
result._total = Object.values(result).reduce((acc: number, val: number) => acc + val, 0);
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"only_one_file_allowed": "Es ist nur eine Datei erlaubt",
|
||||
"only_owners_managers_and_manage_access_members_can_perform_this_action": "Nur Eigentümer, Manager und Mitglieder mit Zugriff auf das Management können diese Aktion ausführen.",
|
||||
"option_id": "Option-ID",
|
||||
"option_ids": "Option-IDs",
|
||||
"or": "oder",
|
||||
"organization": "Organisation",
|
||||
"organization_id": "Organisations-ID",
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"only_one_file_allowed": "Only one file is allowed",
|
||||
"only_owners_managers_and_manage_access_members_can_perform_this_action": "Only owners and managers can perform this action.",
|
||||
"option_id": "Option ID",
|
||||
"option_ids": "Option IDs",
|
||||
"or": "or",
|
||||
"organization": "Organization",
|
||||
"organization_id": "Organization ID",
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"only_one_file_allowed": "Un seul fichier est autorisé",
|
||||
"only_owners_managers_and_manage_access_members_can_perform_this_action": "Seules les propriétaires, les gestionnaires et les membres ayant accès à la gestion peuvent effectuer cette action.",
|
||||
"option_id": "Identifiant de l'option",
|
||||
"option_ids": "Identifiants des options",
|
||||
"or": "ou",
|
||||
"organization": "Organisation",
|
||||
"organization_id": "ID de l'organisation",
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"only_one_file_allowed": "É permitido apenas um arquivo",
|
||||
"only_owners_managers_and_manage_access_members_can_perform_this_action": "Apenas proprietários, gerentes e membros com acesso de gerenciamento podem realizar essa ação.",
|
||||
"option_id": "ID da opção",
|
||||
"option_ids": "IDs da Opção",
|
||||
"or": "ou",
|
||||
"organization": "organização",
|
||||
"organization_id": "ID da Organização",
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"only_one_file_allowed": "Apenas um ficheiro é permitido",
|
||||
"only_owners_managers_and_manage_access_members_can_perform_this_action": "Apenas proprietários e gestores podem realizar esta ação.",
|
||||
"option_id": "ID de Opção",
|
||||
"option_ids": "IDs de Opção",
|
||||
"or": "ou",
|
||||
"organization": "Organização",
|
||||
"organization_id": "ID da Organização",
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"only_one_file_allowed": "僅允許一個檔案",
|
||||
"only_owners_managers_and_manage_access_members_can_perform_this_action": "只有擁有者、管理員和管理存取權限的成員才能執行此操作。",
|
||||
"option_id": "選項 ID",
|
||||
"option_ids": "選項 IDs",
|
||||
"or": "或",
|
||||
"organization": "組織",
|
||||
"organization_id": "組織 ID",
|
||||
|
||||
@@ -12,9 +12,10 @@ vi.mock("@/modules/ui/components/file-upload-response", () => ({
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/picture-selection-response", () => ({
|
||||
PictureSelectionResponse: ({ selected, isExpanded }: any) => (
|
||||
<div data-testid="PictureSelectionResponse">
|
||||
PictureSelection: {selected.join(",")} ({isExpanded ? "expanded" : "collapsed"})
|
||||
PictureSelectionResponse: ({ selected, isExpanded, showId }: any) => (
|
||||
<div data-testid="PictureSelectionResponse" data-show-id={showId}>
|
||||
PictureSelection: {selected.join(",")} ({isExpanded ? "expanded" : "collapsed"}) showId:{" "}
|
||||
{String(showId)}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -22,10 +23,28 @@ vi.mock("@/modules/ui/components/array-response", () => ({
|
||||
ArrayResponse: ({ value }: any) => <div data-testid="ArrayResponse">{value.join(",")}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/response-badges", () => ({
|
||||
ResponseBadges: ({ items }: any) => <div data-testid="ResponseBadges">{items.join(",")}</div>,
|
||||
ResponseBadges: ({ items, showId }: any) => (
|
||||
<div data-testid="ResponseBadges" data-show-id={showId}>
|
||||
{Array.isArray(items)
|
||||
? items
|
||||
.map((item) => (typeof item === "object" ? `${item.value}:${item.id || "no-id"}` : item))
|
||||
.join(",")
|
||||
: items}{" "}
|
||||
showId: {String(showId)}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/ranking-response", () => ({
|
||||
RankingResponse: ({ value }: any) => <div data-testid="RankingResponse">{value.join(",")}</div>,
|
||||
RankingResponse: ({ value, showId }: any) => (
|
||||
<div data-testid="RankingResponse" data-show-id={showId}>
|
||||
{Array.isArray(value)
|
||||
? value
|
||||
.map((item) => (typeof item === "object" ? `${item.value}:${item.id || "no-id"}` : item))
|
||||
.join(",")
|
||||
: value}{" "}
|
||||
showId: {String(showId)}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/analysis/utils", () => ({
|
||||
renderHyperlinkedContent: vi.fn((text: string) => "hyper:" + text),
|
||||
@@ -50,7 +69,14 @@ describe("RenderResponse", () => {
|
||||
});
|
||||
|
||||
const defaultSurvey = { languages: [] } as any;
|
||||
const defaultQuestion = { id: "q1", type: "Unknown" } as any;
|
||||
const defaultQuestion = {
|
||||
id: "q1",
|
||||
type: "Unknown",
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option 1" } },
|
||||
{ id: "choice2", label: { default: "Option 2" } },
|
||||
],
|
||||
} as any;
|
||||
const dummyLanguage = "default";
|
||||
|
||||
test("returns '-' for empty responseData (string)", () => {
|
||||
@@ -60,6 +86,7 @@ describe("RenderResponse", () => {
|
||||
question={defaultQuestion}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(container.textContent).toBe("-");
|
||||
@@ -72,6 +99,7 @@ describe("RenderResponse", () => {
|
||||
question={defaultQuestion}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(container.textContent).toBe("-");
|
||||
@@ -84,6 +112,7 @@ describe("RenderResponse", () => {
|
||||
question={defaultQuestion}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(container.textContent).toBe("-");
|
||||
@@ -92,7 +121,13 @@ describe("RenderResponse", () => {
|
||||
test("renders RatingResponse for 'Rating' question with number", () => {
|
||||
const question = { ...defaultQuestion, type: "rating", scale: 5, range: [1, 5] };
|
||||
render(
|
||||
<RenderResponse responseData={4} question={question} survey={defaultSurvey} language={dummyLanguage} />
|
||||
<RenderResponse
|
||||
responseData={4}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("RatingResponse")).toHaveTextContent("Rating: 4");
|
||||
});
|
||||
@@ -106,6 +141,7 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/formatted_/)).toBeInTheDocument();
|
||||
@@ -119,6 +155,7 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("PictureSelectionResponse")).toHaveTextContent(
|
||||
@@ -134,6 +171,7 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("FileUploadResponse")).toHaveTextContent("FileUpload: file1,file2");
|
||||
@@ -149,6 +187,7 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={{ languages: [] } as any}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("row1:processed:answer1")).toBeInTheDocument();
|
||||
@@ -163,6 +202,7 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ArrayResponse")).toHaveTextContent("addr1,addr2");
|
||||
@@ -176,6 +216,7 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Value");
|
||||
@@ -184,7 +225,13 @@ describe("RenderResponse", () => {
|
||||
test("renders ResponseBadges for 'Consent' question (number)", () => {
|
||||
const question = { ...defaultQuestion, type: "consent" };
|
||||
render(
|
||||
<RenderResponse responseData={5} question={question} survey={defaultSurvey} language={dummyLanguage} />
|
||||
<RenderResponse
|
||||
responseData={5}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("5");
|
||||
});
|
||||
@@ -197,56 +244,67 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Click");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'MultipleChoiceSingle' question (string)", () => {
|
||||
const question = { ...defaultQuestion, type: "multipleChoiceSingle" };
|
||||
const question = { ...defaultQuestion, type: "multipleChoiceSingle", choices: [] };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={"option1"}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("option1");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'MultipleChoiceMulti' question (array)", () => {
|
||||
const question = { ...defaultQuestion, type: "multipleChoiceMulti" };
|
||||
const question = { ...defaultQuestion, type: "multipleChoiceMulti", choices: [] };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["opt1", "opt2"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("opt1,opt2");
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("opt1:other,opt2:other");
|
||||
});
|
||||
|
||||
test("renders ResponseBadges for 'NPS' question (number)", () => {
|
||||
const question = { ...defaultQuestion, type: "nps" };
|
||||
render(
|
||||
<RenderResponse responseData={9} question={question} survey={defaultSurvey} language={dummyLanguage} />
|
||||
<RenderResponse
|
||||
responseData={9}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("9");
|
||||
// NPS questions render as simple text, not ResponseBadges
|
||||
expect(screen.getByText("9")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders RankingResponse for 'Ranking' question", () => {
|
||||
const question = { ...defaultQuestion, type: "ranking" };
|
||||
const question = { ...defaultQuestion, type: "ranking", choices: [] };
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["first", "second"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("RankingResponse")).toHaveTextContent("first,second");
|
||||
expect(screen.getByTestId("RankingResponse")).toHaveTextContent("first:other,second:other showId: false");
|
||||
});
|
||||
|
||||
test("renders default branch for unknown question type with string", () => {
|
||||
@@ -257,6 +315,7 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("hyper:some text")).toBeInTheDocument();
|
||||
@@ -270,8 +329,178 @@ describe("RenderResponse", () => {
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("a, b")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// New tests for showId functionality
|
||||
test("passes showId prop to PictureSelectionResponse", () => {
|
||||
const question = {
|
||||
...defaultQuestion,
|
||||
type: "pictureSelection",
|
||||
choices: [{ id: "choice1", imageUrl: "url1" }],
|
||||
};
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["choice1"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={true}
|
||||
/>
|
||||
);
|
||||
const component = screen.getByTestId("PictureSelectionResponse");
|
||||
expect(component).toHaveAttribute("data-show-id", "true");
|
||||
expect(component).toHaveTextContent("showId: true");
|
||||
});
|
||||
|
||||
test("passes showId prop to RankingResponse with choice ID extraction", () => {
|
||||
const question = {
|
||||
...defaultQuestion,
|
||||
type: "ranking",
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option 1" } },
|
||||
{ id: "choice2", label: { default: "Option 2" } },
|
||||
],
|
||||
};
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["Option 1", "Option 2"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={true}
|
||||
/>
|
||||
);
|
||||
const component = screen.getByTestId("RankingResponse");
|
||||
expect(component).toHaveAttribute("data-show-id", "true");
|
||||
expect(component).toHaveTextContent("showId: true");
|
||||
// Should extract choice IDs and pass them as value objects
|
||||
expect(component).toHaveTextContent("Option 1:choice1,Option 2:choice2");
|
||||
});
|
||||
|
||||
test("handles ranking response with missing choice IDs", () => {
|
||||
const question = {
|
||||
...defaultQuestion,
|
||||
type: "ranking",
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option 1" } },
|
||||
{ id: "choice2", label: { default: "Option 2" } },
|
||||
],
|
||||
};
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["Option 1", "Unknown Option"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={true}
|
||||
/>
|
||||
);
|
||||
const component = screen.getByTestId("RankingResponse");
|
||||
expect(component).toHaveTextContent("Option 1:choice1,Unknown Option:other");
|
||||
});
|
||||
|
||||
test("passes showId prop to ResponseBadges for multiple choice single", () => {
|
||||
const question = {
|
||||
...defaultQuestion,
|
||||
type: "multipleChoiceSingle",
|
||||
choices: [{ id: "choice1", label: { default: "Option 1" } }],
|
||||
};
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={"Option 1"}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={true}
|
||||
/>
|
||||
);
|
||||
const component = screen.getByTestId("ResponseBadges");
|
||||
expect(component).toHaveAttribute("data-show-id", "true");
|
||||
expect(component).toHaveTextContent("showId: true");
|
||||
expect(component).toHaveTextContent("Option 1:choice1");
|
||||
});
|
||||
|
||||
test("passes showId prop to ResponseBadges for multiple choice multi", () => {
|
||||
const question = {
|
||||
...defaultQuestion,
|
||||
type: "multipleChoiceMulti",
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option 1" } },
|
||||
{ id: "choice2", label: { default: "Option 2" } },
|
||||
],
|
||||
};
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["Option 1", "Option 2"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={true}
|
||||
/>
|
||||
);
|
||||
const component = screen.getByTestId("ResponseBadges");
|
||||
expect(component).toHaveAttribute("data-show-id", "true");
|
||||
expect(component).toHaveTextContent("showId: true");
|
||||
expect(component).toHaveTextContent("Option 1:choice1,Option 2:choice2");
|
||||
});
|
||||
|
||||
test("handles multiple choice with missing choice IDs", () => {
|
||||
const question = {
|
||||
...defaultQuestion,
|
||||
type: "multipleChoiceMulti",
|
||||
choices: [{ id: "choice1", label: { default: "Option 1" } }],
|
||||
};
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["Option 1", "Unknown Option"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={true}
|
||||
/>
|
||||
);
|
||||
const component = screen.getByTestId("ResponseBadges");
|
||||
expect(component).toHaveTextContent("Option 1:choice1,Unknown Option:other");
|
||||
});
|
||||
|
||||
test("passes showId=false to components when showId is false", () => {
|
||||
const question = {
|
||||
...defaultQuestion,
|
||||
type: "multipleChoiceMulti",
|
||||
choices: [{ id: "choice1", label: { default: "Option 1" } }],
|
||||
};
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={["Option 1"]}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
const component = screen.getByTestId("ResponseBadges");
|
||||
expect(component).toHaveAttribute("data-show-id", "false");
|
||||
expect(component).toHaveTextContent("showId: false");
|
||||
// Should still extract IDs but showId=false
|
||||
expect(component).toHaveTextContent("Option 1:choice1");
|
||||
});
|
||||
|
||||
test("handles questions without choices property", () => {
|
||||
const question = { ...defaultQuestion, type: "multipleChoiceSingle" }; // No choices property
|
||||
render(
|
||||
<RenderResponse
|
||||
responseData={"Option 1"}
|
||||
question={question}
|
||||
survey={defaultSurvey}
|
||||
language={dummyLanguage}
|
||||
showId={true}
|
||||
/>
|
||||
);
|
||||
const component = screen.getByTestId("ResponseBadges");
|
||||
expect(component).toHaveTextContent("Option 1:choice1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cn } from "@/lib/cn";
|
||||
import { getLanguageCode, getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { processResponseData } from "@/lib/responses";
|
||||
import { formatDateWithOrdinal } from "@/lib/utils/datetime";
|
||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||
@@ -27,6 +28,7 @@ interface RenderResponseProps {
|
||||
survey: TSurvey;
|
||||
language: string | null;
|
||||
isExpanded?: boolean;
|
||||
showId: boolean;
|
||||
}
|
||||
|
||||
export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
@@ -35,6 +37,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
survey,
|
||||
language,
|
||||
isExpanded = true,
|
||||
showId,
|
||||
}) => {
|
||||
if (
|
||||
(typeof responseData === "string" && responseData === "") ||
|
||||
@@ -81,6 +84,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
choices={(question as TSurveyPictureSelectionQuestion).choices}
|
||||
selected={responseData}
|
||||
isExpanded={isExpanded}
|
||||
showId={showId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -121,9 +125,10 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
if (typeof responseData === "string" || typeof responseData === "number") {
|
||||
return (
|
||||
<ResponseBadges
|
||||
items={[capitalizeFirstLetter(responseData.toString())]}
|
||||
items={[{ value: capitalizeFirstLetter(responseData.toString()) }]}
|
||||
isExpanded={isExpanded}
|
||||
icon={<PhoneIcon className="h-4 w-4 text-slate-500" />}
|
||||
showId={showId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -132,9 +137,10 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
if (typeof responseData === "string" || typeof responseData === "number") {
|
||||
return (
|
||||
<ResponseBadges
|
||||
items={[capitalizeFirstLetter(responseData.toString())]}
|
||||
items={[{ value: capitalizeFirstLetter(responseData.toString()) }]}
|
||||
isExpanded={isExpanded}
|
||||
icon={<CheckCheckIcon className="h-4 w-4 text-slate-500" />}
|
||||
showId={showId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -143,26 +149,43 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
if (typeof responseData === "string" || typeof responseData === "number") {
|
||||
return (
|
||||
<ResponseBadges
|
||||
items={[capitalizeFirstLetter(responseData.toString())]}
|
||||
items={[{ value: capitalizeFirstLetter(responseData.toString()) }]}
|
||||
isExpanded={isExpanded}
|
||||
icon={<MousePointerClickIcon className="h-4 w-4 text-slate-500" />}
|
||||
showId={showId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceMulti:
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyQuestionTypeEnum.NPS:
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
if (typeof responseData === "string" || typeof responseData === "number") {
|
||||
return <ResponseBadges items={[responseData.toString()]} isExpanded={isExpanded} />;
|
||||
const choiceId = getChoiceIdByValue(responseData.toString(), question);
|
||||
return (
|
||||
<ResponseBadges
|
||||
items={[{ value: responseData.toString(), id: choiceId }]}
|
||||
isExpanded={isExpanded}
|
||||
showId={showId}
|
||||
/>
|
||||
);
|
||||
} else if (Array.isArray(responseData)) {
|
||||
return <ResponseBadges items={responseData} isExpanded={isExpanded} />;
|
||||
const itemsArray = responseData.map((choice) => {
|
||||
const choiceId = getChoiceIdByValue(choice, question);
|
||||
return { value: choice, id: choiceId };
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{questionType === TSurveyQuestionTypeEnum.Ranking ? (
|
||||
<RankingResponse value={itemsArray} isExpanded={isExpanded} showId={showId} />
|
||||
) : (
|
||||
<ResponseBadges items={itemsArray} isExpanded={isExpanded} showId={showId} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
if (Array.isArray(responseData)) {
|
||||
return <RankingResponse value={responseData} isExpanded={isExpanded} />;
|
||||
}
|
||||
|
||||
default:
|
||||
if (
|
||||
typeof responseData === "string" ||
|
||||
|
||||
@@ -76,7 +76,7 @@ export const SingleResponseCardBody = ({
|
||||
<div key={`${question.id}`}>
|
||||
{isValidValue(response.data[question.id]) ? (
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">
|
||||
<p className="mb-1 text-sm text-slate-500">
|
||||
{formatTextWithSlashes(
|
||||
parseRecallInfo(
|
||||
getLocalizedValue(question.headline, "default"),
|
||||
@@ -92,6 +92,7 @@ export const SingleResponseCardBody = ({
|
||||
survey={survey}
|
||||
responseData={response.data[question.id]}
|
||||
language={response.language}
|
||||
showId={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
@@ -162,19 +163,21 @@ export const SingleResponseCardHeader = ({
|
||||
{response.contact?.id ? (
|
||||
user ? (
|
||||
<Link
|
||||
className="flex items-center"
|
||||
className="flex items-center space-x-2"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
{response.contact.userId && <IdBadge id={response.contact.userId} />}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
{response.contact.userId && <IdBadge id={response.contact.userId} />}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
|
||||
@@ -43,6 +43,16 @@ vi.mock("@/modules/survey/editor/components/conditional-logic", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/editor/components/option-ids", () => ({
|
||||
OptionIds: ({ question, selectedLanguageCode }: any) => (
|
||||
<div data-testid="option-ids">
|
||||
<span data-testid="option-ids-question-id">{question.id}</span>
|
||||
<span data-testid="option-ids-question-type">{question.type}</span>
|
||||
<span data-testid="option-ids-language-code">{selectedLanguageCode}</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/editor/components/update-question-id", () => ({
|
||||
UpdateQuestionId: ({ question, questionIdx, localSurvey, updateQuestion }: any) => (
|
||||
<div data-testid="update-question-id">
|
||||
@@ -110,6 +120,7 @@ describe("AdvancedSettings", () => {
|
||||
questionIdx={questionIdx}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
selectedLanguageCode="en"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -175,6 +186,7 @@ describe("AdvancedSettings", () => {
|
||||
questionIdx={questionIdx}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
selectedLanguageCode="fr"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -259,6 +271,7 @@ describe("AdvancedSettings", () => {
|
||||
questionIdx={questionIdx}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={mockUpdateQuestion}
|
||||
selectedLanguageCode="de"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -409,4 +422,230 @@ describe("AdvancedSettings", () => {
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledTimes(2);
|
||||
expect(mockUpdateQuestion).toHaveBeenLastCalledWith(1, { id: "new-id" });
|
||||
});
|
||||
|
||||
// New tests for OptionIds functionality
|
||||
test("renders OptionIds component for multiple choice single questions", () => {
|
||||
const mockQuestion = {
|
||||
id: "mc-question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Multiple Choice Question" },
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option 1" } },
|
||||
{ id: "choice2", label: { default: "Option 2" } },
|
||||
],
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey1",
|
||||
questions: [mockQuestion],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<AdvancedSettings
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={vi.fn()}
|
||||
selectedLanguageCode="en"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("option-ids")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("option-ids-question-id")).toHaveTextContent("mc-question");
|
||||
expect(screen.getByTestId("option-ids-question-type")).toHaveTextContent("multipleChoiceSingle");
|
||||
expect(screen.getByTestId("option-ids-language-code")).toHaveTextContent("en");
|
||||
});
|
||||
|
||||
test("renders OptionIds component for multiple choice multi questions", () => {
|
||||
const mockQuestion = {
|
||||
id: "mcm-question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Multiple Choice Multi Question" },
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option A" } },
|
||||
{ id: "choice2", label: { default: "Option B" } },
|
||||
{ id: "choice3", label: { default: "Option C" } },
|
||||
],
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey2",
|
||||
questions: [mockQuestion],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<AdvancedSettings
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={vi.fn()}
|
||||
selectedLanguageCode="fr"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("option-ids")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("option-ids-question-id")).toHaveTextContent("mcm-question");
|
||||
expect(screen.getByTestId("option-ids-question-type")).toHaveTextContent("multipleChoiceMulti");
|
||||
expect(screen.getByTestId("option-ids-language-code")).toHaveTextContent("fr");
|
||||
});
|
||||
|
||||
test("renders OptionIds component for picture selection questions", () => {
|
||||
const mockQuestion = {
|
||||
id: "pic-question",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
headline: { default: "Picture Selection Question" },
|
||||
choices: [
|
||||
{ id: "pic1", imageUrl: "https://example.com/img1.jpg" },
|
||||
{ id: "pic2", imageUrl: "https://example.com/img2.jpg" },
|
||||
],
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey3",
|
||||
questions: [mockQuestion],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<AdvancedSettings
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={vi.fn()}
|
||||
selectedLanguageCode="de"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("option-ids")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("option-ids-question-id")).toHaveTextContent("pic-question");
|
||||
expect(screen.getByTestId("option-ids-question-type")).toHaveTextContent("pictureSelection");
|
||||
expect(screen.getByTestId("option-ids-language-code")).toHaveTextContent("de");
|
||||
});
|
||||
|
||||
test("renders OptionIds component for ranking questions", () => {
|
||||
const mockQuestion = {
|
||||
id: "rank-question",
|
||||
type: TSurveyQuestionTypeEnum.Ranking,
|
||||
headline: { default: "Ranking Question" },
|
||||
choices: [
|
||||
{ id: "rank1", label: { default: "First Option" } },
|
||||
{ id: "rank2", label: { default: "Second Option" } },
|
||||
{ id: "rank3", label: { default: "Third Option" } },
|
||||
],
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey4",
|
||||
questions: [mockQuestion],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<AdvancedSettings
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={vi.fn()}
|
||||
selectedLanguageCode="es"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("option-ids")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("option-ids-question-id")).toHaveTextContent("rank-question");
|
||||
expect(screen.getByTestId("option-ids-question-type")).toHaveTextContent("ranking");
|
||||
expect(screen.getByTestId("option-ids-language-code")).toHaveTextContent("es");
|
||||
});
|
||||
|
||||
test("does not render OptionIds component for non-choice question types", () => {
|
||||
const openTextQuestion = {
|
||||
id: "open-text-question",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Open Text Question" },
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey5",
|
||||
questions: [openTextQuestion],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<AdvancedSettings
|
||||
question={openTextQuestion}
|
||||
questionIdx={0}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={vi.fn()}
|
||||
selectedLanguageCode="en"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("option-ids")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("conditional-logic")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("update-question-id")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render OptionIds component for rating questions", () => {
|
||||
const ratingQuestion = {
|
||||
id: "rating-question",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rating Question" },
|
||||
scale: 5,
|
||||
range: [1, 5],
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey6",
|
||||
questions: [ratingQuestion],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
render(
|
||||
<AdvancedSettings
|
||||
question={ratingQuestion}
|
||||
questionIdx={0}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={vi.fn()}
|
||||
selectedLanguageCode="en"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("option-ids")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("conditional-logic")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("update-question-id")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("passes correct selectedLanguageCode to OptionIds component", () => {
|
||||
const mockQuestion = {
|
||||
id: "test-question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Test Question" },
|
||||
choices: [{ id: "choice1", label: { default: "Option 1" } }],
|
||||
} as unknown as TSurveyQuestion;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey8",
|
||||
questions: [mockQuestion],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const { rerender } = render(
|
||||
<AdvancedSettings
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={vi.fn()}
|
||||
selectedLanguageCode="ja"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("option-ids-language-code")).toHaveTextContent("ja");
|
||||
|
||||
// Test with different language code
|
||||
rerender(
|
||||
<AdvancedSettings
|
||||
question={mockQuestion}
|
||||
questionIdx={0}
|
||||
localSurvey={mockSurvey}
|
||||
updateQuestion={vi.fn()}
|
||||
selectedLanguageCode="zh"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("option-ids-language-code")).toHaveTextContent("zh");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ConditionalLogic } from "@/modules/survey/editor/components/conditional-logic";
|
||||
import { OptionIds } from "@/modules/survey/editor/components/option-ids";
|
||||
import { UpdateQuestionId } from "@/modules/survey/editor/components/update-question-id";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface AdvancedSettingsProps {
|
||||
question: TSurveyQuestion;
|
||||
questionIdx: number;
|
||||
localSurvey: TSurvey;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
selectedLanguageCode: string;
|
||||
}
|
||||
|
||||
export const AdvancedSettings = ({
|
||||
@@ -14,7 +16,14 @@ export const AdvancedSettings = ({
|
||||
questionIdx,
|
||||
localSurvey,
|
||||
updateQuestion,
|
||||
selectedLanguageCode,
|
||||
}: AdvancedSettingsProps) => {
|
||||
const showOptionIds =
|
||||
question.type === TSurveyQuestionTypeEnum.PictureSelection ||
|
||||
question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ||
|
||||
question.type === TSurveyQuestionTypeEnum.Ranking;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<ConditionalLogic
|
||||
@@ -30,6 +39,8 @@ export const AdvancedSettings = ({
|
||||
localSurvey={localSurvey}
|
||||
updateQuestion={updateQuestion}
|
||||
/>
|
||||
|
||||
{showOptionIds && <OptionIds question={question} selectedLanguageCode={selectedLanguageCode} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
177
apps/web/modules/survey/editor/components/option-ids.test.tsx
Normal file
177
apps/web/modules/survey/editor/components/option-ids.test.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { OptionIds } from "./option-ids";
|
||||
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: vi.fn((label: any, _languageCode: string) => label.default || ""),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/id-badge", () => ({
|
||||
IdBadge: ({ id, label }: { id: string; label?: string }) => (
|
||||
<div data-testid="id-badge" data-id={id} data-label={label}>
|
||||
{label ? `${label}: ${id}` : id}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/label", () => ({
|
||||
Label: ({
|
||||
children,
|
||||
htmlFor,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
htmlFor?: string;
|
||||
className?: string;
|
||||
}) => (
|
||||
<label htmlFor={htmlFor} className={className} data-testid="label">
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("next/image", () => ({
|
||||
__esModule: true,
|
||||
default: ({ src, alt, className }: { src: string; alt: string; className: string }) => (
|
||||
<img src={src} alt={alt} className={className} data-testid="choice-image" />
|
||||
),
|
||||
}));
|
||||
|
||||
describe("OptionIds", () => {
|
||||
const mockMultipleChoiceQuestion: TSurveyQuestion = {
|
||||
id: "question1",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Test Question" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Option 1" } },
|
||||
{ id: "choice2", label: { default: "Option 2" } },
|
||||
{ id: "choice3", label: { default: "Option 3" } },
|
||||
],
|
||||
};
|
||||
|
||||
const mockRankingQuestion: TSurveyQuestion = {
|
||||
id: "question2",
|
||||
type: TSurveyQuestionTypeEnum.Ranking,
|
||||
headline: { default: "Ranking Question" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "rank1", label: { default: "First Choice" } },
|
||||
{ id: "rank2", label: { default: "Second Choice" } },
|
||||
],
|
||||
};
|
||||
|
||||
const mockPictureSelectionQuestion: TSurveyQuestion = {
|
||||
id: "question3",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
headline: { default: "Picture Question" },
|
||||
required: false,
|
||||
allowMulti: false,
|
||||
choices: [
|
||||
{ id: "pic1", imageUrl: "https://example.com/image1.jpg" },
|
||||
{ id: "pic2", imageUrl: "https://example.com/image2.jpg" },
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders multiple choice question option IDs", () => {
|
||||
render(<OptionIds question={mockMultipleChoiceQuestion} selectedLanguageCode="en" />);
|
||||
|
||||
expect(screen.getByTestId("label")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.option_ids")).toBeInTheDocument();
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(3);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "choice1");
|
||||
expect(idBadges[0]).toHaveAttribute("data-label", "Option 1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "choice2");
|
||||
expect(idBadges[1]).toHaveAttribute("data-label", "Option 2");
|
||||
expect(idBadges[2]).toHaveAttribute("data-id", "choice3");
|
||||
expect(idBadges[2]).toHaveAttribute("data-label", "Option 3");
|
||||
});
|
||||
|
||||
test("renders multiple choice multi question option IDs", () => {
|
||||
const multiChoiceQuestion = {
|
||||
...mockMultipleChoiceQuestion,
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
} as TSurveyQuestion;
|
||||
|
||||
render(<OptionIds question={multiChoiceQuestion} selectedLanguageCode="en" />);
|
||||
|
||||
expect(screen.getByTestId("label")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.option_ids")).toBeInTheDocument();
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("renders ranking question option IDs", () => {
|
||||
render(<OptionIds question={mockRankingQuestion} selectedLanguageCode="en" />);
|
||||
|
||||
expect(screen.getByTestId("label")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.option_ids")).toBeInTheDocument();
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "rank1");
|
||||
expect(idBadges[0]).toHaveAttribute("data-label", "First Choice");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "rank2");
|
||||
expect(idBadges[1]).toHaveAttribute("data-label", "Second Choice");
|
||||
});
|
||||
|
||||
test("renders picture selection question option IDs with images", () => {
|
||||
render(<OptionIds question={mockPictureSelectionQuestion} selectedLanguageCode="en" />);
|
||||
|
||||
expect(screen.getByTestId("label")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.option_ids")).toBeInTheDocument();
|
||||
|
||||
const images = screen.getAllByTestId("choice-image");
|
||||
expect(images).toHaveLength(2);
|
||||
expect(images[0]).toHaveAttribute("src", "https://example.com/image1.jpg");
|
||||
expect(images[0]).toHaveAttribute("alt", "Choice pic1");
|
||||
expect(images[1]).toHaveAttribute("src", "https://example.com/image2.jpg");
|
||||
expect(images[1]).toHaveAttribute("alt", "Choice pic2");
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "pic1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "pic2");
|
||||
});
|
||||
|
||||
test("handles picture selection with missing imageUrl", () => {
|
||||
const questionWithMissingImage = {
|
||||
...mockPictureSelectionQuestion,
|
||||
choices: [
|
||||
{ id: "pic1", imageUrl: "https://example.com/image1.jpg" },
|
||||
{ id: "pic2", imageUrl: "" },
|
||||
],
|
||||
} as TSurveyQuestion;
|
||||
|
||||
render(<OptionIds question={questionWithMissingImage} selectedLanguageCode="en" />);
|
||||
|
||||
const images = screen.getAllByTestId("choice-image");
|
||||
expect(images).toHaveLength(1);
|
||||
expect(images[0]).toHaveAttribute("src", "https://example.com/image1.jpg");
|
||||
// Next.js Image component doesn't render src attribute when imageUrl is empty
|
||||
});
|
||||
|
||||
test("uses correct language code for localized values", () => {
|
||||
const getLocalizedValueMock = vi.mocked(getLocalizedValue);
|
||||
|
||||
render(<OptionIds question={mockMultipleChoiceQuestion} selectedLanguageCode="fr" />);
|
||||
|
||||
expect(getLocalizedValueMock).toHaveBeenCalledWith({ default: "Option 1" }, "fr");
|
||||
expect(getLocalizedValueMock).toHaveBeenCalledWith({ default: "Option 2" }, "fr");
|
||||
expect(getLocalizedValueMock).toHaveBeenCalledWith({ default: "Option 3" }, "fr");
|
||||
});
|
||||
});
|
||||
68
apps/web/modules/survey/editor/components/option-ids.tsx
Normal file
68
apps/web/modules/survey/editor/components/option-ids.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import Image from "next/image";
|
||||
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface OptionIdsProps {
|
||||
question: TSurveyQuestion;
|
||||
selectedLanguageCode: string;
|
||||
}
|
||||
|
||||
export const OptionIds = ({ question, selectedLanguageCode }: OptionIdsProps) => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
const renderChoiceIds = () => {
|
||||
switch (question.type) {
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceMulti:
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{question.choices.map((choice) => (
|
||||
<div key={choice.id}>
|
||||
<IdBadge id={choice.id} label={getLocalizedValue(choice.label, selectedLanguageCode)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case TSurveyQuestionTypeEnum.PictureSelection:
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{question.choices.map((choice) => {
|
||||
const imageUrl = choice.imageUrl;
|
||||
if (!imageUrl) return null;
|
||||
return (
|
||||
<div key={choice.id} className="flex items-center gap-3">
|
||||
<div className="relative h-24 w-40 overflow-hidden rounded-lg bg-gray-100">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={`Choice ${choice.id}`}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 160px"
|
||||
style={{ objectFit: "cover" }}
|
||||
quality={75}
|
||||
className="rounded-lg transition-opacity duration-200"
|
||||
/>
|
||||
</div>
|
||||
<IdBadge id={choice.id} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-gray-700">{t("common.option_ids")}</Label>
|
||||
<div className="w-full">{renderChoiceIds()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -559,6 +559,7 @@ export const QuestionCard = ({
|
||||
questionIdx={questionIdx}
|
||||
localSurvey={localSurvey}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
/>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { PictureSelectionResponse } from "./index";
|
||||
|
||||
@@ -7,7 +7,16 @@ import { PictureSelectionResponse } from "./index";
|
||||
vi.mock("next/image", () => ({
|
||||
__esModule: true,
|
||||
default: ({ src, alt, className }: { src: string; alt: string; className: string }) => (
|
||||
<img src={src} alt={alt} className={className} />
|
||||
<img src={src} alt={alt} className={className} data-testid="choice-image" />
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the IdBadge component
|
||||
vi.mock("@/modules/ui/components/id-badge", () => ({
|
||||
IdBadge: ({ id }: { id: string }) => (
|
||||
<div data-testid="id-badge" data-id={id}>
|
||||
ID: {id}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -33,7 +42,7 @@ describe("PictureSelectionResponse", () => {
|
||||
|
||||
test("renders images for selected choices", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice3"]} />
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice3"]} showId={false} />
|
||||
);
|
||||
|
||||
const images = container.querySelectorAll("img");
|
||||
@@ -44,13 +53,20 @@ describe("PictureSelectionResponse", () => {
|
||||
|
||||
test("renders nothing when selected is not an array", () => {
|
||||
// @ts-ignore - Testing invalid prop type
|
||||
const { container } = render(<PictureSelectionResponse choices={mockChoices} selected="choice1" />);
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected="choice1" showId={false} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test("handles expanded layout", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice2"]} isExpanded={true} />
|
||||
<PictureSelectionResponse
|
||||
choices={mockChoices}
|
||||
selected={["choice1", "choice2"]}
|
||||
isExpanded={true}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
@@ -59,7 +75,12 @@ describe("PictureSelectionResponse", () => {
|
||||
|
||||
test("handles non-expanded layout", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice2"]} isExpanded={false} />
|
||||
<PictureSelectionResponse
|
||||
choices={mockChoices}
|
||||
selected={["choice1", "choice2"]}
|
||||
isExpanded={false}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
@@ -68,10 +89,75 @@ describe("PictureSelectionResponse", () => {
|
||||
|
||||
test("handles choices not in the mapping", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "nonExistentChoice"]} />
|
||||
<PictureSelectionResponse
|
||||
choices={mockChoices}
|
||||
selected={["choice1", "nonExistentChoice"]}
|
||||
showId={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const images = container.querySelectorAll("img");
|
||||
expect(images).toHaveLength(1); // Only one valid image should be rendered
|
||||
});
|
||||
|
||||
test("shows IdBadge when showId=true", () => {
|
||||
render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice2"]} showId={true} />
|
||||
);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "choice1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "choice2");
|
||||
});
|
||||
|
||||
test("does not show IdBadge when showId=false", () => {
|
||||
render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice2"]} showId={false} />
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("id-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies column layout when showId=true", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1"]} showId={true} />
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass("flex-col");
|
||||
});
|
||||
|
||||
test("does not apply column layout when showId=false", () => {
|
||||
const { container } = render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1"]} showId={false} />
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).not.toHaveClass("flex-col");
|
||||
});
|
||||
|
||||
test("renders images and IdBadges in same container when showId=true", () => {
|
||||
render(
|
||||
<PictureSelectionResponse choices={mockChoices} selected={["choice1", "choice2"]} showId={true} />
|
||||
);
|
||||
|
||||
const images = screen.getAllByTestId("choice-image");
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
|
||||
expect(images).toHaveLength(2);
|
||||
expect(idBadges).toHaveLength(2);
|
||||
|
||||
// Both images and badges should be in the same container
|
||||
const containers = screen.getAllByText("ID: choice1")[0].closest("div");
|
||||
expect(containers).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles default props correctly", () => {
|
||||
render(<PictureSelectionResponse choices={mockChoices} selected={["choice1"]} showId={false} />);
|
||||
|
||||
const images = screen.getAllByTestId("choice-image");
|
||||
expect(images).toHaveLength(1);
|
||||
expect(screen.queryByTestId("id-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import Image from "next/image";
|
||||
|
||||
interface PictureSelectionResponseProps {
|
||||
choices: { id: string; imageUrl: string }[];
|
||||
selected: string | number | string[];
|
||||
isExpanded?: boolean;
|
||||
showId: boolean;
|
||||
}
|
||||
|
||||
export const PictureSelectionResponse = ({
|
||||
choices,
|
||||
selected,
|
||||
isExpanded = true,
|
||||
showId,
|
||||
}: PictureSelectionResponseProps) => {
|
||||
if (typeof selected !== "object") return null;
|
||||
|
||||
@@ -25,17 +28,22 @@ export const PictureSelectionResponse = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("my-1 flex gap-x-5 gap-y-4", isExpanded ? "flex-wrap" : "")}>
|
||||
<div className={cn("my-1 flex gap-x-5 gap-y-4", isExpanded ? "flex-wrap" : "", showId ? "flex-col" : "")}>
|
||||
{selected.map((id) => (
|
||||
<div className="relative h-10 w-16" key={id}>
|
||||
<div className="flex items-center gap-2" key={id}>
|
||||
{choiceImageMapping[id] && (
|
||||
<Image
|
||||
src={choiceImageMapping[id]}
|
||||
alt={choiceImageMapping[id].split("/").pop() || "Image"}
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
<>
|
||||
<div className="relative h-10 w-16">
|
||||
<Image
|
||||
src={choiceImageMapping[id]}
|
||||
alt={choiceImageMapping[id].split("/").pop() || "Image"}
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
{showId && <IdBadge id={id} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { RankingResponse } from "./index";
|
||||
|
||||
// Mock the IdBadge component
|
||||
vi.mock("@/modules/ui/components/id-badge", () => ({
|
||||
IdBadge: ({ id }: { id: string }) => (
|
||||
<div data-testid="id-badge" data-id={id}>
|
||||
ID: {id}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("RankingResponse", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders ranked items correctly", () => {
|
||||
const rankedItems = ["Apple", "Banana", "Cherry"];
|
||||
test("renders ranked items correctly with new object format", () => {
|
||||
const rankedItems = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "Banana", id: "choice2" },
|
||||
{ value: "Cherry", id: "choice3" },
|
||||
];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} showId={false} />);
|
||||
|
||||
expect(screen.getByText("#1")).toBeInTheDocument();
|
||||
expect(screen.getByText("#2")).toBeInTheDocument();
|
||||
@@ -21,10 +34,57 @@ describe("RankingResponse", () => {
|
||||
expect(screen.getByText("Cherry")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies expanded layout", () => {
|
||||
const rankedItems = ["Apple", "Banana"];
|
||||
test("renders ranked items with undefined id", () => {
|
||||
const rankedItems = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "Banana", id: undefined },
|
||||
{ value: "Cherry", id: "choice3" },
|
||||
];
|
||||
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} showId={true} />);
|
||||
|
||||
expect(screen.getByText("Apple")).toBeInTheDocument();
|
||||
expect(screen.getByText("Banana")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cherry")).toBeInTheDocument();
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2); // Only items with defined ids should have badges
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "choice1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "choice3");
|
||||
});
|
||||
|
||||
test("shows IdBadge when showId=true", () => {
|
||||
const rankedItems = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "Banana", id: "choice2" },
|
||||
];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} showId={true} />);
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "choice1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "choice2");
|
||||
});
|
||||
|
||||
test("does not show IdBadge when showId=false", () => {
|
||||
const rankedItems = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "Banana", id: "choice2" },
|
||||
];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} showId={false} />);
|
||||
|
||||
expect(screen.queryByTestId("id-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies expanded layout", () => {
|
||||
const rankedItems = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "Banana", id: "choice2" },
|
||||
];
|
||||
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={true} showId={false} />);
|
||||
|
||||
const parentDiv = container.firstChild;
|
||||
expect(parentDiv).not.toHaveClass("flex");
|
||||
@@ -32,19 +92,44 @@ describe("RankingResponse", () => {
|
||||
});
|
||||
|
||||
test("applies non-expanded layout", () => {
|
||||
const rankedItems = ["Apple", "Banana"];
|
||||
const rankedItems = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "Banana", id: "choice2" },
|
||||
];
|
||||
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={false} />);
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={false} showId={false} />);
|
||||
|
||||
const parentDiv = container.firstChild;
|
||||
expect(parentDiv).toHaveClass("flex");
|
||||
expect(parentDiv).toHaveClass("space-x-2");
|
||||
});
|
||||
|
||||
test("handles empty values", () => {
|
||||
const rankedItems = ["Apple", "", "Cherry"];
|
||||
test("applies column layout when showId=true", () => {
|
||||
const rankedItems = [{ value: "Apple", id: "choice1" }];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={true} showId={true} />);
|
||||
|
||||
const parentDiv = container.firstChild;
|
||||
expect(parentDiv).toHaveClass("flex-col");
|
||||
});
|
||||
|
||||
test("does not apply column layout when showId=false", () => {
|
||||
const rankedItems = [{ value: "Apple", id: "choice1" }];
|
||||
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={true} showId={false} />);
|
||||
|
||||
const parentDiv = container.firstChild;
|
||||
expect(parentDiv).not.toHaveClass("flex-col");
|
||||
});
|
||||
|
||||
test("handles empty values", () => {
|
||||
const rankedItems = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "", id: "choice2" },
|
||||
{ value: "Cherry", id: "choice3" },
|
||||
];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} showId={false} />);
|
||||
|
||||
expect(screen.getByText("#1")).toBeInTheDocument();
|
||||
expect(screen.getByText("#3")).toBeInTheDocument();
|
||||
@@ -54,9 +139,13 @@ describe("RankingResponse", () => {
|
||||
});
|
||||
|
||||
test("displays items in the correct order", () => {
|
||||
const rankedItems = ["First", "Second", "Third"];
|
||||
const rankedItems = [
|
||||
{ value: "First", id: "choice1" },
|
||||
{ value: "Second", id: "choice2" },
|
||||
{ value: "Third", id: "choice3" },
|
||||
];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} showId={false} />);
|
||||
|
||||
const rankNumbers = screen.getAllByText(/^#\d$/);
|
||||
const rankItems = screen.getAllByText(/(First|Second|Third)/);
|
||||
@@ -72,11 +161,33 @@ describe("RankingResponse", () => {
|
||||
});
|
||||
|
||||
test("renders with RTL support", () => {
|
||||
const rankedItems = ["תפוח", "בננה", "דובדבן"];
|
||||
const rankedItems = [
|
||||
{ value: "תפוח", id: "choice1" },
|
||||
{ value: "בננה", id: "choice2" },
|
||||
{ value: "דובדבן", id: "choice3" },
|
||||
];
|
||||
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={true} />);
|
||||
const { container } = render(<RankingResponse value={rankedItems} isExpanded={true} showId={false} />);
|
||||
|
||||
const parentDiv = container.firstChild as HTMLElement;
|
||||
expect(parentDiv).toHaveAttribute("dir", "auto");
|
||||
});
|
||||
|
||||
test("renders items and badges together when showId=true", () => {
|
||||
const rankedItems = [
|
||||
{ value: "First", id: "choice1" },
|
||||
{ value: "Second", id: "choice2" },
|
||||
];
|
||||
|
||||
render(<RankingResponse value={rankedItems} isExpanded={true} showId={true} />);
|
||||
|
||||
// Check that both items and badges are rendered
|
||||
expect(screen.getByText("First")).toBeInTheDocument();
|
||||
expect(screen.getByText("Second")).toBeInTheDocument();
|
||||
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "choice1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "choice2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { cn } from "@/lib/cn";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
|
||||
interface RankingResponseProps {
|
||||
value: string[];
|
||||
value: { value: string; id: string | undefined }[];
|
||||
isExpanded: boolean;
|
||||
showId: boolean;
|
||||
}
|
||||
|
||||
export const RankingResponse = ({ value, isExpanded }: RankingResponseProps) => {
|
||||
export const RankingResponse = ({ value, isExpanded, showId }: RankingResponseProps) => {
|
||||
return (
|
||||
<div className={cn("my-1 font-semibold text-slate-700", isExpanded ? "" : "flex space-x-2")} dir="auto">
|
||||
<div
|
||||
className={cn("text-slate-700", isExpanded ? "space-y-2" : "flex space-x-2", showId ? "flex-col" : "")}
|
||||
dir="auto">
|
||||
{value.map(
|
||||
(item, index) =>
|
||||
item && (
|
||||
<div key={index} className="mb-1 flex items-center">
|
||||
<span className="mr-2 text-slate-400">#{index + 1}</span>
|
||||
<div className="rounded bg-slate-100 px-2 py-1">{item}</div>
|
||||
item.value && (
|
||||
<div key={item.value} className="flex items-center space-x-2">
|
||||
<span className="text-slate-400">#{index + 1}</span>
|
||||
<div className="rounded bg-slate-100 px-2 py-1 font-semibold">{item.value}</div>
|
||||
{item.id && showId && <IdBadge id={item.id} />}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { ResponseBadges } from "./index";
|
||||
|
||||
// Mock the IdBadge component
|
||||
vi.mock("@/modules/ui/components/id-badge", () => ({
|
||||
IdBadge: ({ id }: { id: string }) => (
|
||||
<div data-testid="id-badge" data-id={id}>
|
||||
ID: {id}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("ResponseBadges", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders string items correctly", () => {
|
||||
const items = ["Apple", "Banana", "Cherry"];
|
||||
render(<ResponseBadges items={items} />);
|
||||
test("renders items with value property correctly", () => {
|
||||
const items = [{ value: "Apple" }, { value: "Banana" }, { value: "Cherry" }];
|
||||
render(<ResponseBadges items={items} showId={false} />);
|
||||
|
||||
expect(screen.getByText("Apple")).toBeInTheDocument();
|
||||
expect(screen.getByText("Banana")).toBeInTheDocument();
|
||||
@@ -28,37 +37,109 @@ describe("ResponseBadges", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("renders number items correctly", () => {
|
||||
const items = [1, 2, 3];
|
||||
render(<ResponseBadges items={items} />);
|
||||
test("renders number items with value property correctly", () => {
|
||||
const items = [{ value: 1 }, { value: 2 }, { value: 3 }];
|
||||
render(<ResponseBadges items={items} showId={false} />);
|
||||
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
expect(screen.getByText("2")).toBeInTheDocument();
|
||||
expect(screen.getByText("3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies expanded layout when isExpanded=true", () => {
|
||||
const items = ["Apple", "Banana", "Cherry"];
|
||||
test("renders items with id property and shows IdBadge when showId=true", () => {
|
||||
const items = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "Banana", id: "choice2" },
|
||||
{ value: "Cherry" }, // No id property
|
||||
];
|
||||
render(<ResponseBadges items={items} showId={true} />);
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} isExpanded={true} />);
|
||||
expect(screen.getByText("Apple")).toBeInTheDocument();
|
||||
expect(screen.getByText("Banana")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cherry")).toBeInTheDocument();
|
||||
|
||||
// Should show IdBadges for items with id
|
||||
const idBadges = screen.getAllByTestId("id-badge");
|
||||
expect(idBadges).toHaveLength(2);
|
||||
expect(idBadges[0]).toHaveAttribute("data-id", "choice1");
|
||||
expect(idBadges[1]).toHaveAttribute("data-id", "choice2");
|
||||
});
|
||||
|
||||
test("does not render IdBadge when showId=false", () => {
|
||||
const items = [
|
||||
{ value: "Apple", id: "choice1" },
|
||||
{ value: "Banana", id: "choice2" },
|
||||
];
|
||||
render(<ResponseBadges items={items} showId={false} />);
|
||||
|
||||
expect(screen.getByText("Apple")).toBeInTheDocument();
|
||||
expect(screen.getByText("Banana")).toBeInTheDocument();
|
||||
|
||||
// Should not show IdBadges when showId=false
|
||||
expect(screen.queryByTestId("id-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render IdBadge when item has no id property", () => {
|
||||
const items = [{ value: "Apple" }, { value: "Banana" }];
|
||||
render(<ResponseBadges items={items} showId={true} />);
|
||||
|
||||
expect(screen.getByText("Apple")).toBeInTheDocument();
|
||||
expect(screen.getByText("Banana")).toBeInTheDocument();
|
||||
|
||||
// Should not show IdBadges when items have no id
|
||||
expect(screen.queryByTestId("id-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies expanded layout when isExpanded=true", () => {
|
||||
const items = [{ value: "Apple" }, { value: "Banana" }, { value: "Cherry" }];
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} isExpanded={true} showId={false} />);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).toHaveClass("flex-wrap");
|
||||
});
|
||||
|
||||
test("does not apply expanded layout when isExpanded=false", () => {
|
||||
const items = ["Apple", "Banana", "Cherry"];
|
||||
const items = [{ value: "Apple" }, { value: "Banana" }, { value: "Cherry" }];
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} isExpanded={false} />);
|
||||
const { container } = render(<ResponseBadges items={items} isExpanded={false} showId={false} />);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).not.toHaveClass("flex-wrap");
|
||||
});
|
||||
|
||||
test("applies default styles correctly", () => {
|
||||
const items = ["Apple"];
|
||||
test("applies column layout when showId=true", () => {
|
||||
const items = [{ value: "Apple", id: "choice1" }];
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} />);
|
||||
const { container } = render(<ResponseBadges items={items} showId={true} />);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).toHaveClass("flex-col");
|
||||
});
|
||||
|
||||
test("does not apply column layout when showId=false", () => {
|
||||
const items = [{ value: "Apple", id: "choice1" }];
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} showId={false} />);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).not.toHaveClass("flex-col");
|
||||
});
|
||||
|
||||
test("renders with icon when provided", () => {
|
||||
const items = [{ value: "Apple" }];
|
||||
const icon = <span data-testid="test-icon">📱</span>;
|
||||
|
||||
render(<ResponseBadges items={items} icon={icon} showId={false} />);
|
||||
|
||||
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText("📱")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies default styles correctly", () => {
|
||||
const items = [{ value: "Apple" }];
|
||||
|
||||
const { container } = render(<ResponseBadges items={items} showId={false} />);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).toHaveClass("my-1");
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { cn } from "@/lib/cn";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import React from "react";
|
||||
|
||||
interface ResponseBadgesProps {
|
||||
items: string[] | number[];
|
||||
items: { value: string | number; id?: string }[];
|
||||
isExpanded?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
showId: boolean;
|
||||
}
|
||||
|
||||
export const ResponseBadges: React.FC<ResponseBadgesProps> = ({ items, isExpanded = false, icon }) => {
|
||||
export const ResponseBadges: React.FC<ResponseBadgesProps> = ({
|
||||
items,
|
||||
isExpanded = false,
|
||||
icon,
|
||||
showId,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("my-1 flex gap-2", isExpanded ? "flex-wrap" : "")}>
|
||||
<div className={cn("my-1 flex gap-2", isExpanded ? "flex-wrap" : "", showId ? "flex-col" : "")}>
|
||||
{items.map((item, index) => (
|
||||
<span key={index} className="flex items-center rounded-md bg-slate-200 px-2 py-1 font-medium">
|
||||
{icon && <span className="mr-1.5">{icon}</span>}
|
||||
{item}
|
||||
</span>
|
||||
<div key={`${item.value}-${index}`} className={cn("flex items-center gap-2")}>
|
||||
<span className="flex items-center rounded-md bg-slate-200 px-2 py-1 font-medium">
|
||||
{icon && <span className="mr-1.5">{icon}</span>}
|
||||
{item.value}
|
||||
</span>
|
||||
{item.id && showId && <IdBadge id={item.id} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -141,8 +141,8 @@ test.describe("JS Package Test", async () => {
|
||||
await expect(page.getByRole("button", { name: "Completed 100%" })).toBeVisible();
|
||||
await expect(page.getByText("1 Responses", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText("CTR100%")).toBeVisible();
|
||||
await expect(page.getByText("Somewhat disappointed100%")).toBeVisible();
|
||||
await expect(page.getByText("Founder100%")).toBeVisible();
|
||||
await expect(page.getByText("Somewhat disappointed")).toBeVisible();
|
||||
await expect(page.getByText("Founder")).toBeVisible();
|
||||
await expect(page.getByText("People who believe that PMF").first()).toBeVisible();
|
||||
await expect(page.getByText("Much higher response rates!").first()).toBeVisible();
|
||||
await expect(page.getByText("Make this end to end test").first()).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user