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
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();
|
|
});
|
|
});
|