feat: surface option ids (#6339)

This commit is contained in:
Dhruwang Jariwala
2025-08-05 09:33:12 +05:30
committed by GitHub
parent 3b07a6d013
commit 287c45f996
31 changed files with 1689 additions and 275 deletions
@@ -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>
);