Browse Source
Merge branch 'staging' of https://git.nwhco.ir/sina_sajjadi/marriage into staging
staging
Merge branch 'staging' of https://git.nwhco.ir/sina_sajjadi/marriage into staging
staging
30 changed files with 643 additions and 128 deletions
-
126src/components/Componentes/question-file.tsx
-
213src/components/Componentes/question-photo.test.tsx
-
190src/components/Componentes/question-photo.tsx
-
2src/components/Componentes/question-section-flow.tsx
-
1src/hooks/marriage/use-upload-tmp-media.ts
-
71src/lib/http.ts
-
7src/translations/locales/ar.json
-
7src/translations/locales/az.json
-
7src/translations/locales/bn.json
-
7src/translations/locales/da.json
-
7src/translations/locales/de.json
-
7src/translations/locales/en.json
-
7src/translations/locales/es.json
-
7src/translations/locales/fa.json
-
7src/translations/locales/fr.json
-
7src/translations/locales/gu.json
-
7src/translations/locales/ha.json
-
7src/translations/locales/he.json
-
7src/translations/locales/hi.json
-
7src/translations/locales/id.json
-
7src/translations/locales/ks.json
-
7src/translations/locales/pt.json
-
7src/translations/locales/ru.json
-
7src/translations/locales/sw.json
-
7src/translations/locales/tg.json
-
7src/translations/locales/tr.json
-
7src/translations/locales/ul.json
-
7src/translations/locales/ur.json
-
7src/translations/locales/uz.json
-
7src/translations/locales/zh.json
@ -0,0 +1,213 @@ |
|||||
|
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"], |
||||
|
}, |
||||
|
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(() => { |
||||
|
expect(screen.getByText("Photo upload failed. Please try again.")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
// Answer value is cleared
|
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockPhotoQuestion, null); |
||||
|
|
||||
|
// Default avatar SVG placeholder is restored
|
||||
|
expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); |
||||
|
|
||||
|
// Camera badge is not visible
|
||||
|
expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeNull(); |
||||
|
|
||||
|
// File input is reset allowing re-upload
|
||||
|
expect(fileInput.value).toBe(""); |
||||
|
} finally { |
||||
|
URL.createObjectURL = originalCreateObjectURL; |
||||
|
URL.revokeObjectURL = originalRevokeObjectURL; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
it("shows error toast and reverts to default avatar when rendered image fails to load", 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); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Failed to load image. Please try uploading again.")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
// Answer value cleared
|
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockPhotoQuestion, null); |
||||
|
|
||||
|
// Default avatar placeholder rendered instead of broken image
|
||||
|
expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); |
||||
|
expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue