3 changed files with 25 additions and 255 deletions
-
16next.config.ts
-
206src/components/Componentes/question-photo.test.tsx
-
58src/components/Componentes/question-photo.tsx
@ -1,206 +0,0 @@ |
|||
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<string, unknown> = {}; |
|||
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(<QuestionPhoto question={mockPhotoQuestion} />); |
|||
|
|||
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(<QuestionPhoto question={mockPhotoQuestion} />); |
|||
|
|||
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(<QuestionPhoto question={mockPhotoQuestion} />); |
|||
|
|||
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(<QuestionPhoto question={mockPhotoQuestion} />); |
|||
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(<QuestionPhoto question={mockPhotoQuestion} />); |
|||
|
|||
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(); |
|||
}); |
|||
}); |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue