import {
render,
screen,
cleanup,
fireEvent,
waitFor,
} from "@testing-library/react";
import { describe, it, expect, vi, afterEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { QuestionAnswersProvider } from "./question-answer-storage";
import { QuestionBirthplace } from "./question-birthplace";
import QuestionNumber from "./question-number";
import QuestionText from "./question-text";
vi.mock("@/translations/provider", () => ({
useI18n: vi.fn(() => ({ locale: "fa", dictionary: {} })),
}));
describe("UI Config based behavior", () => {
afterEach(() => {
cleanup();
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
it("should trigger GeoIP only when ui_config.enable_geoip is true", () => {
// With totally random title but ui_config.enable_geoip = true
const qWithGeo = {
id: "q1",
title: "Random Title Here",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: {},
options: [],
ui_config: { enable_geoip: true },
} as any;
const { rerender } = render(
,
);
// Auto button is present when GeoIP is active
expect(screen.getByText("خودکار")).toBeDefined();
// With title "residence" but no ui_config
const qWithoutGeo = {
id: "q2",
title: "residence test",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: {},
options: [],
ui_config: {},
} as any;
rerender(
,
);
expect(screen.queryByText("خودکار")).toBeNull();
});
it("should auto-detect and display city and country when ui_config.enable_geoip is true", async () => {
const qWithGeo = {
id: "q_residence",
title: "Current Residence",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: {},
options: [],
ui_config: { enable_geoip: true },
} as any;
const { setStoredUserGeoRegion } = await import("@/lib/geo-region");
const { marriageQueryKeys } = await import("@/hooks/marriage/query-keys");
setStoredUserGeoRegion({
city: "Tehran",
country: "Iran",
countryCode: "IR",
phoneCode: "+98",
});
queryClient.setQueryData(marriageQueryKeys.sectionData("test-geoip"), {
status: "success",
data: [],
version: 1,
});
render(
,
);
await waitFor(() => {
expect(screen.getByText(/(ایران|Iran).*Tehran/i)).toBeDefined();
});
});
it("should trigger currency behavior only when ui_config.currency_enabled is true", () => {
// Title is random, but currency_enabled is true
const qWithCurrency = {
id: "q1",
title: "Random Income",
type: "number",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "", range: [0, 0] },
options: [],
ui_config: { currency_enabled: true },
} as any;
const { rerender } = render(
,
);
// Dropdown arrow should be rendered in currency mode (path is in SVG)
expect(screen.getByRole("img", { name: "Dropdown chevron" })).toBeDefined();
// Title is "Monthly Income", but currency_enabled is false
const qWithoutCurrency = {
id: "q2",
title: "Monthly Income",
type: "number",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "", range: [0, 0] },
options: [],
ui_config: {},
} as any;
rerender(
,
);
expect(screen.queryByRole("img", { name: "Dropdown chevron" })).toBeNull();
});
it("should render error message from validation.errorMessage or fallback to locale", async () => {
// Title is random, type is email, custom error message
const qCustomError = {
id: "q1",
title: "Random Email",
type: "email",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "" },
options: [],
validation: { errorMessage: "Custom Backend Error" },
} as any;
const { rerender } = render(
,
);
const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "invalid_email" } });
await waitFor(() => {
expect(screen.getByText("Custom Backend Error")).toBeDefined();
});
const qFallbackError = {
id: "q2",
title: "Random Email",
type: "email",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "" },
options: [],
validation: {},
} as any;
rerender(
,
);
const input2 = screen.getByRole("textbox");
fireEvent.change(input2, { target: { value: "invalid_email_again" } });
await waitFor(() => {
expect(screen.getByText("یک آدرس ایمیل معتبر وارد کنید.")).toBeDefined();
});
});
it("should preserve city input when country is selected or changed in birthplace", async () => {
const qBirthplace = {
id: "birthplace_test",
title: "Birthplace",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "City, region" },
options: [],
ui_config: {},
} as any;
render(
,
);
// Type city into input
const cityInput = screen.getByPlaceholderText("City, region");
expect(document.activeElement).not.toBe(cityInput);
fireEvent.change(cityInput, { target: { value: "Tehran" } });
expect((cityInput as HTMLInputElement).value).toBe("Tehran");
// Open country dropdown
const countryButton = screen.getByText("انتخاب کشور");
fireEvent.click(countryButton);
expect(document.body.classList.contains("dropdown-open")).toBe(true);
// Select Iran
const iranOption = screen.getByRole("button", { name: "ایران" });
fireEvent.click(iranOption);
// City should still be Tehran!
expect((cityInput as HTMLInputElement).value).toBe("Tehran");
await waitFor(() => {
expect(document.activeElement).toBe(cityInput);
expect(document.body.classList.contains("dropdown-open")).toBe(false);
});
});
it("should preserve country when typing city after selecting country", async () => {
const qBirthplace = {
id: "birthplace_select_first",
title: "محل تولد",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "شهر، منطقه یا محله" },
options: [],
ui_config: {},
} as any;
render(
,
);
// 1. Open country dropdown first
const countryButton = screen.getByText("انتخاب کشور");
fireEvent.click(countryButton);
// 2. Select Germany (آلمان)
const germanyOption = screen.getByRole("button", { name: "آلمان" });
fireEvent.click(germanyOption);
await waitFor(() => {
expect(screen.queryByRole("dialog")).toBeNull();
});
// Country button should show Germany
expect(screen.getByText("آلمان")).toBeDefined();
// 3. Type city in the input field
const cityInput = screen.getByPlaceholderText("شهر، منطقه یا محله");
fireEvent.change(cityInput, { target: { value: "برلین" } });
// Country button MUST still show Germany and city must show Berlin
expect(screen.getByText("آلمان")).toBeDefined();
expect((cityInput as HTMLInputElement).value).toBe("برلین");
// 4. Type more characters into city field
fireEvent.change(cityInput, { target: { value: "برلین مرکزی" } });
expect(screen.getByText("آلمان")).toBeDefined();
expect((cityInput as HTMLInputElement).value).toBe("برلین مرکزی");
// 5. Change country to France (فرانسه)
const updatedCountryButton = screen.getByText("آلمان");
fireEvent.click(updatedCountryButton);
const franceOption = screen.getByRole("button", { name: "فرانسه" });
fireEvent.click(franceOption);
await waitFor(() => {
expect(screen.queryByRole("dialog")).toBeNull();
});
expect(screen.getByText("فرانسه")).toBeDefined();
expect((cityInput as HTMLInputElement).value).toBe("برلین مرکزی");
});
it("should not duplicate or alter city text when focused and typing continuously", async () => {
const qBirthplace = {
id: "birthplace_typing_test",
title: "محل تولد",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "شهر، منطقه یا محله" },
options: [],
ui_config: {},
} as any;
render(
,
);
const cityInput = screen.getByPlaceholderText("شهر، منطقه یا محله");
fireEvent.focus(cityInput);
// Simulate character-by-character typing: ت -> ته -> تهر -> تهرا -> تهران
fireEvent.change(cityInput, { target: { value: "ت" } });
expect((cityInput as HTMLInputElement).value).toBe("ت");
fireEvent.change(cityInput, { target: { value: "ته" } });
expect((cityInput as HTMLInputElement).value).toBe("ته");
fireEvent.change(cityInput, { target: { value: "تهر" } });
expect((cityInput as HTMLInputElement).value).toBe("تهر");
fireEvent.change(cityInput, { target: { value: "تهرا" } });
expect((cityInput as HTMLInputElement).value).toBe("تهرا");
fireEvent.change(cityInput, { target: { value: "تهران" } });
expect((cityInput as HTMLInputElement).value).toBe("تهران");
fireEvent.blur(cityInput);
expect((cityInput as HTMLInputElement).value).toBe("تهران");
});
it("should allow typing and clearing city in current_residence manual mode without duplication", async () => {
const qResidence = {
id: "residence_manual_typing_test",
title: "محل سکونت فعلی",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "شهر، منطقه یا محله" },
options: [],
ui_config: { enable_geoip: true },
} as any;
render(
,
);
// Switch to manual mode
const manualButton = screen.getByRole("button", { name: /دستی|Manual/i });
fireEvent.click(manualButton);
const cityInput = screen.getByPlaceholderText("شهر، منطقه یا محله");
fireEvent.focus(cityInput);
// Type "اصفهان"
fireEvent.change(cityInput, { target: { value: "اصفهان" } });
expect((cityInput as HTMLInputElement).value).toBe("اصفهان");
// Clear the input completely
fireEvent.change(cityInput, { target: { value: "" } });
expect((cityInput as HTMLInputElement).value).toBe("");
fireEvent.blur(cityInput);
expect((cityInput as HTMLInputElement).value).toBe("");
// Type again "شیراز"
fireEvent.focus(cityInput);
fireEvent.change(cityInput, { target: { value: "شیراز" } });
expect((cityInput as HTMLInputElement).value).toBe("شیراز");
fireEvent.blur(cityInput);
expect((cityInput as HTMLInputElement).value).toBe("شیراز");
});
it("should preserve manual mode and custom entered city upon section re-entry", async () => {
const qResidence = {
id: "residence_persistence_test",
title: "محل سکونت فعلی",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "شهر، منطقه یا محله" },
options: [],
ui_config: { enable_geoip: true },
} as any;
const { unmount } = render(
,
);
// Click Manual
const manualBtn = screen.getByRole("button", { name: /دستی|Manual/i });
fireEvent.click(manualBtn);
const cityInput = screen.getByPlaceholderText("شهر، منطقه یا محله");
fireEvent.change(cityInput, { target: { value: "یزد" } });
fireEvent.blur(cityInput);
expect((cityInput as HTMLInputElement).value).toBe("یزد");
// Simulate leaving the section (unmount)
unmount();
const { marriageQueryKeys } = await import("@/hooks/marriage/query-keys");
queryClient.setQueryData(
marriageQueryKeys.formSection("profile", "test-persistence", "fa"),
{
section: {
id: "sec1",
slug: "test-persistence",
title: "Sec",
cards: [{ id: "c1", title: "Card", questions: [qResidence] }],
},
answers: {
residence_persistence_test: {
value: { country: "Iran", city: "یزد" },
},
},
},
);
// Simulate re-entering the section
render(
,
);
// Verify it stays in manual mode with "یزد"
await waitFor(() => {
const reenteredCityInput = screen.getByPlaceholderText("شهر، منطقه یا محله");
expect((reenteredCityInput as HTMLInputElement).value).toBe("یزد");
});
});
it("should normalize ui_config for photo and file questions with source and picker_type", async () => {
const { mapBackendQuestionToFrontend } = await import("@/lib/schema-adapter");
const photoBackendQuestion: any = {
id: "verification.face_photo",
type: "photo",
title: "عکس پرسنلی یا واضح",
required: true,
is_visible: true,
ui_config: {},
};
const adaptedPhoto = mapBackendQuestionToFrontend(photoBackendQuestion, 0);
expect(adaptedPhoto.ui_config?.source).toBe("gallery");
expect(adaptedPhoto.ui_config?.picker_type).toBe("gallery");
const fileBackendQuestion: any = {
id: "verification.identity_document",
type: "file",
title: "بارگذاری مدرک هویتی",
required: true,
is_visible: true,
ui_config: {},
};
const adaptedFile = mapBackendQuestionToFrontend(fileBackendQuestion, 1);
expect(adaptedFile.ui_config?.source).toBe("file_system");
expect(adaptedFile.ui_config?.picker_type).toBe("file_system");
});
it("should preserve custom source and picker_type from backend ui_config", async () => {
const { mapBackendQuestionToFrontend } = await import("@/lib/schema-adapter");
const customPhoto: any = {
id: "verification.face_photo",
type: "photo",
title: "عکس سلفی دوربین",
required: true,
is_visible: true,
ui_config: { source: "camera", picker_type: "camera" },
};
const adaptedCustomPhoto = mapBackendQuestionToFrontend(customPhoto, 0);
expect(adaptedCustomPhoto.ui_config?.source).toBe("camera");
expect(adaptedCustomPhoto.ui_config?.picker_type).toBe("camera");
});
it("should make country and city fields display-only specifically for current_residence", async () => {
const qResidence = {
id: "contact_residence.current_residence_location",
title: "Current Place of Residence (Country, City / State)",
type: "birthplace",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "City, State" },
options: [],
ui_config: { enable_geoip: true, location_kind: "residence" },
} as any;
const { unmount } = render(
,
);
// Switch to manual mode to reveal fields
const manualBtn = screen.getByRole("button", { name: /دستی|Manual/i });
fireEvent.click(manualBtn);
// Country button should be read-only / not open sheet on click
const countryBtn = screen.getByRole("button", { name: /ایران|Iran|Turkmenistan/i });
expect(countryBtn.getAttribute("aria-readonly")).toBe("true");
fireEvent.click(countryBtn);
expect(screen.queryByRole("dialog")).toBeNull();
// City input should be readOnly
const cityInput = screen.getByPlaceholderText("City, State");
expect((cityInput as HTMLInputElement).readOnly).toBe(true);
unmount();
// Verify birthplace question remains fully editable
const qBirthplace = {
id: "personal_identity.birthplace",
title: "Place of Birth (Country, City / State)",
type: "birthplace",
order: 2,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "City, State" },
options: [],
ui_config: { enable_geoip: false, location_kind: "birthplace" },
} as any;
render(
,
);
const bpCountryBtn = screen.getByRole("button", {
name: /انتخاب کشور|Select country/i,
});
expect(bpCountryBtn.getAttribute("aria-readonly")).not.toBe("true");
const bpCityInput = screen.getByPlaceholderText("City, State");
expect((bpCityInput as HTMLInputElement).readOnly).toBe(false);
});
});