You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

206 lines
7.1 KiB

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