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