import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { QuestionField } from "@/lib/schema-adapter"; import { QuestionPhoto } from "./question-photo"; let answerMap: Record = {}; const mockSetAnswerValue = vi.fn((q: QuestionField, val: unknown) => { answerMap[q.id] = val; }); const mockRegisterPendingUpload = vi.fn(); const mockUploadTmpMedia = vi.fn(async (file: File) => ({ path: `/media/tmp/${file.name}`, name: file.name, })); vi.mock("@/hooks/marriage/use-upload-tmp-media", () => ({ useUploadTmpMediaMutation: ({ onSuccess, onError, }: { onSuccess?: (data: any) => void; onError?: (err: any) => void; }) => ({ mutate: (file: File) => { mockUploadTmpMedia(file) .then((res) => onSuccess?.(res)) .catch((err) => onError?.(err)); }, mutateAsync: async (file: File) => { try { const res = await mockUploadTmpMedia(file); onSuccess?.(res); return res; } catch (err) { onError?.(err); throw err; } }, isPending: false, isError: false, }), })); vi.mock("./question-answer-storage", () => ({ useQuestionAnswers: () => ({ getAnswerValue: (q: QuestionField) => answerMap[q.id] ?? null, setAnswerValue: mockSetAnswerValue, registerPendingUpload: mockRegisterPendingUpload, isLoading: false, }), })); vi.mock("@/translations/provider", () => ({ useI18n: () => ({ locale: "en", dictionary: { "Photo upload failed. Please try again.": "Photo upload failed. Please try again.", "Failed to load image. Please try uploading again.": "Failed to load image. Please try uploading again.", }, }), })); const mockPhotoQuestion: QuestionField = { id: "documents_verification.face_photo", title: "Face Photo", type: "photo", order: 1, required: true, baseRequired: true, isVisible: true, description: "Recent clear face photo", tooltip: "", extras: { options: [".jpg", ".jpeg", ".png"], } as any, options: [], }; describe("QuestionPhoto Component", () => { beforeEach(() => { answerMap = {}; mockSetAnswerValue.mockClear(); mockUploadTmpMedia.mockClear(); mockRegisterPendingUpload.mockClear(); vi.clearAllMocks(); }); afterEach(() => { cleanup(); }); it("renders default avatar placeholder when no photo uploaded", () => { const { container } = render(); expect(screen.getByText(/Face/)).toBeInTheDocument(); expect(screen.getByText(/Photo/)).toBeInTheDocument(); expect(screen.getByText("Recent clear face photo")).toBeInTheDocument(); // Default placeholder SVG is rendered expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); }); it("renders uploaded image and camera badge when answer is provided", () => { answerMap[mockPhotoQuestion.id] = "https://example.com/avatar.jpg"; const { container } = render(); const img = container.querySelector("img[src='https://example.com/avatar.jpg']"); expect(img).toBeInTheDocument(); expect(img).toHaveClass("rounded-full"); // Camera badge rendered expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeInTheDocument(); }); it("triggers upload and renders circular spinner without black background overlay during file selection", async () => { let resolveUpload!: (val: any) => void; mockUploadTmpMedia.mockImplementationOnce( () => new Promise((resolve) => { resolveUpload = resolve; }), ); const { container } = render(); const fileInput = container.querySelector("input[type='file']") as HTMLInputElement; const testFile = new File(["dummy content"], "my-photo.jpg", { type: "image/jpeg" }); const originalCreateObjectURL = URL.createObjectURL; URL.createObjectURL = vi.fn(() => "blob:http://localhost/test-blob"); try { fireEvent.change(fileInput, { target: { files: [testFile] } }); await waitFor(() => { const spinner = container.querySelector(".animate-spin"); expect(spinner).toBeInTheDocument(); expect(spinner).toHaveClass("border-[#F0445B]/25"); }); expect(container.querySelector(".bg-black\\/40")).toBeNull(); expect(container.querySelector(".bg-black")).toBeNull(); resolveUpload({ path: "/media/uploaded.jpg" }); await waitFor(() => { expect(container.querySelector(".animate-spin")).toBeNull(); }); } finally { URL.createObjectURL = originalCreateObjectURL; } }); it("shows error toast, reverts to default avatar placeholder, and clears answer when upload fails", async () => { mockUploadTmpMedia.mockImplementationOnce( () => Promise.reject(new Error("Network Error")), ); const { container } = render(); const fileInput = container.querySelector("input[type='file']") as HTMLInputElement; const testFile = new File(["dummy content"], "bad-photo.jpg", { type: "image/jpeg" }); const originalCreateObjectURL = URL.createObjectURL; const originalRevokeObjectURL = URL.revokeObjectURL; URL.createObjectURL = vi.fn(() => "blob:http://localhost/bad-blob"); URL.revokeObjectURL = vi.fn(); try { fireEvent.change(fileInput, { target: { files: [testFile] } }); // Error toast should appear await waitFor(() => { // Toast message should appear expect(screen.getByText("Photo upload failed. Please try again.")).toBeInTheDocument(); // Answer value is NOT cleared — matches yesterday's behavior // (error just shows toast, does not wipe the form state) expect(mockSetAnswerValue).not.toHaveBeenCalledWith(mockPhotoQuestion, null); // File input is reset allowing re-upload expect(fileInput.value).toBe(""); }); } finally { URL.createObjectURL = originalCreateObjectURL; URL.revokeObjectURL = originalRevokeObjectURL; } }); it("keeps stored answer when rendered image fails to load (preview-only failure)", async () => { answerMap[mockPhotoQuestion.id] = "https://example.com/broken-avatar.jpg"; const { container } = render(); const img = container.querySelector("img[src='https://example.com/broken-avatar.jpg']") as HTMLImageElement; expect(img).toBeInTheDocument(); // Trigger image error (e.g. 404 / ERR_CONNECTION_REFUSED) fireEvent.error(img); // The stored answer should NOT be cleared — the file is already on the server. // This matches yesterday's behavior where preview errors never wiped the answer. expect(mockSetAnswerValue).not.toHaveBeenCalledWith(mockPhotoQuestion, null); // The component should remain in "answered" state via data-question-answered // because storedValue still holds the uploaded path expect(container.querySelector("[data-question-answered='true']")).toBeInTheDocument(); }); });