You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

375 lines
12 KiB

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { QuestionField } from "@/lib/schema-adapter";
import { QuestionAnswersProvider } from "./question-answer-storage";
import { QuestionSheet } from "./question-sheet";
vi.mock("@/translations/provider", () => ({
useI18n: vi.fn(() => ({ locale: "fa", dictionary: { Confirm: "تایید" } })),
}));
vi.mock("@/hooks/marriage/use-section-data", () => ({
applyProfilePatchResultToCache: vi.fn(),
useMarriageSectionDataQuery: vi.fn(() => ({
data: undefined,
isLoading: false,
})),
useUpdateMarriageSectionDataMutation: vi.fn(() => ({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
})),
}));
describe("QuestionSheet component", () => {
afterEach(() => {
cleanup();
window.history.replaceState(null, "", window.location.href);
document.body.classList.remove("dropdown-open");
document.body.classList.remove("question-sheet-open");
document.body.classList.remove("question-keyboard-open");
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
it("renders trigger and opens bottom sheet on click", async () => {
const qDropdown = {
id: "q_nat",
title: "تابعیت و شهروندی",
type: "dropdown",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "انتخاب کشور" },
options: [
{ id: "iran", value: "Iran", label: "ایران", order: 1 },
{ id: "germany", value: "Germany", label: "آلمان", order: 2 },
{ id: "canada", value: "Canada", label: "کانادا", order: 3 },
],
ui_config: {},
} as QuestionField;
render(
<div className="app-shell">
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[qDropdown]}>
<QuestionSheet question={qDropdown} />
</QuestionAnswersProvider>
</QueryClientProvider>
</div>,
);
// Initial placeholder displayed in trigger
expect(screen.getByText("انتخاب کشور")).toBeDefined();
// Click trigger to open bottom sheet
const trigger = screen.getByRole("button", { name: /انتخاب کشور/i });
fireEvent.click(trigger);
// Bottom sheet dialog should be visible
expect(screen.getByRole("dialog")).toBeDefined();
expect(document.body.classList.contains("dropdown-open")).toBe(true);
expect(document.body.classList.contains("question-sheet-open")).toBe(true);
expect(
(document.querySelector(".app-shell") as HTMLElement).style.overflowY,
).toBe("hidden");
expect(screen.getByRole("button", { name: "ایران" })).toBeDefined();
expect(screen.getByRole("button", { name: "آلمان" })).toBeDefined();
// Select Iran
const iranBtn = screen.getByRole("button", { name: "ایران" });
fireEvent.click(iranBtn);
// Trigger should now show Iran
await waitFor(() => {
expect(screen.getByText("ایران")).toBeDefined();
expect(document.body.classList.contains("dropdown-open")).toBe(false);
expect(document.body.classList.contains("question-sheet-open")).toBe(
false,
);
expect(
(document.querySelector(".app-shell") as HTMLElement).style.overflowY,
).toBe("");
});
});
it("supports multi-selection with confirm button", async () => {
const qMulti = {
id: "q_lang",
title: "سایر زبان‌ها",
type: "dropdown",
order: 2,
required: false,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "انتخاب زبان‌ها", range: [0, 5] },
options: [
{ id: "en", value: "English", label: "انگلیسی", order: 1 },
{ id: "ar", value: "Arabic", label: "عربی", order: 2 },
{ id: "fr", value: "French", label: "فرانسوی", order: 3 },
],
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[qMulti]}>
<QuestionSheet question={qMulti} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
// Click trigger
const trigger = screen.getByRole("button", { name: /انتخاب زبان‌ها/i });
fireEvent.click(trigger);
// Select English and French
const enBtn = screen.getByRole("button", { name: "انگلیسی" });
const frBtn = screen.getByRole("button", { name: "فرانسوی" });
fireEvent.click(enBtn);
fireEvent.click(frBtn);
// Confirm button should show count
const confirmBtn = screen.getByRole("button", { name: /تایید \(2\)/i });
expect(confirmBtn).toBeDefined();
fireEvent.click(confirmBtn);
// Trigger displays selected items
await waitFor(() => {
expect(screen.getByText("انگلیسی, فرانسوی")).toBeDefined();
});
});
it("closes the inline selector with Escape", async () => {
const question = {
id: "q_drag",
title: "کشور محل اقامت",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب کشور" },
options: [{ id: "iran", value: "Iran", label: "ایران", order: 1 }],
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: /انتخاب کشور/i }));
fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
await waitFor(() => {
expect(screen.queryByRole("dialog")).toBeNull();
expect(document.body.classList.contains("dropdown-open")).toBe(false);
expect(document.body.classList.contains("question-sheet-open")).toBe(
false,
);
});
});
it("closes the sheet instead of leaving the page on browser Back", async () => {
const question = {
id: "q_back",
title: "کشور",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب کشور" },
options: [{ id: "iran", value: "Iran", label: "ایران", order: 1 }],
ui_config: {},
} as QuestionField;
const pushState = vi.spyOn(window.history, "pushState");
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: /انتخاب کشور/i }));
await waitFor(() => expect(pushState).toHaveBeenCalledTimes(1));
window.history.replaceState(null, "", window.location.href);
window.dispatchEvent(new PopStateEvent("popstate"));
await waitFor(() => {
expect(screen.queryByRole("dialog")).toBeNull();
});
pushState.mockRestore();
});
it("opens a searchable sheet for 7+ options without focusing search", () => {
const question = {
id: "q_country",
title: "کشور",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب کشور" },
options: Array.from({ length: 7 }, (_, index) => ({
id: `country-${index}`,
value: `country-${index}`,
label: `کشور ${index + 1}`,
order: index + 1,
})),
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: /انتخاب کشور/i }));
expect(screen.getByPlaceholderText("جستجو...")).not.toHaveFocus();
expect(screen.getByRole("dialog").querySelector("section")).toHaveClass(
"h-[82svh]",
);
});
it("sizes a short options sheet to its content (4 options)", () => {
const question = {
id: "q_short",
title: "انتخاب کوتاه",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب" },
options: Array.from({ length: 4 }, (_, index) => ({
id: `option-${index}`,
value: `option-${index}`,
label: `گزینه ${index + 1}`,
order: index + 1,
})),
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "انتخاب" }));
expect(screen.getByRole("dialog").querySelector("section")).toHaveClass(
"h-auto",
"max-h-[82svh]",
);
});
it("compact boundary: 5 options → compact, no search", () => {
const question = {
id: "q_boundary_5",
title: "پنج گزینه",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب" },
options: Array.from({ length: 5 }, (_, i) => ({
id: `opt-${i}`,
value: `opt-${i}`,
label: `گزینه ${i + 1}`,
order: i + 1,
})),
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "انتخاب" }));
const section = screen.getByRole("dialog").querySelector("section");
expect(section).toHaveClass("h-auto", "max-h-[82svh]");
expect(screen.queryByPlaceholderText("جستجو...")).toBeNull();
});
it("compact boundary: 6 options → compact, no search", () => {
const question = {
id: "q_boundary_6",
title: "شش گزینه",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب" },
options: Array.from({ length: 6 }, (_, i) => ({
id: `opt-${i}`,
value: `opt-${i}`,
label: `گزینه ${i + 1}`,
order: i + 1,
})),
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "انتخاب" }));
const section = screen.getByRole("dialog").querySelector("section");
expect(section).toHaveClass("h-auto", "max-h-[82svh]");
expect(section).not.toHaveClass("h-[82svh]");
expect(screen.queryByPlaceholderText("جستجو...")).toBeNull();
});
it("compact boundary: 7 options → large sheet with search", () => {
const question = {
id: "q_boundary_7",
title: "هفت گزینه",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب" },
options: Array.from({ length: 7 }, (_, i) => ({
id: `opt-${i}`,
value: `opt-${i}`,
label: `گزینه ${i + 1}`,
order: i + 1,
})),
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "انتخاب" }));
const section = screen.getByRole("dialog").querySelector("section");
expect(section).toHaveClass("h-[82svh]");
expect(section).not.toHaveClass("h-auto");
expect(screen.getByPlaceholderText("جستجو...")).toBeDefined();
});
});