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