Browse Source
feat: add multi-language support, implement conditional logic components, and update question schemas
Dev
feat: add multi-language support, implement conditional logic components, and update question schemas
Dev
34 changed files with 1129 additions and 319 deletions
-
2api_questions.json
-
2api_questions_marital.json
-
23conditional-rules.js
-
33src/app/questions-list/[slug]/question-detail-client.tsx
-
224src/components/Componentes/question-file.test.tsx
-
586src/components/Componentes/question-file.tsx
-
50src/components/Componentes/question-sheet.test.tsx
-
96src/components/Componentes/question-sheet.tsx
-
71src/lib/conditional-rules.test.ts
-
41src/lib/conditional-rules.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
2
api_questions.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
api_questions_marital.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,224 @@ |
|||||
|
import { |
||||
|
act, |
||||
|
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 { QuestionFile } from "./question-file"; |
||||
|
|
||||
|
let answerMap: Record<string, unknown> = {}; |
||||
|
const mockSetAnswerValue = vi.fn((q: QuestionField, val: unknown) => { |
||||
|
answerMap[q.id] = val; |
||||
|
}); |
||||
|
|
||||
|
const mockMutateAsync = vi.fn(async (file: File) => ({ |
||||
|
path: `/media/tmp/${file.name}`, |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/marriage/use-upload-tmp-media", () => ({ |
||||
|
useUploadTmpMediaMutation: () => ({ |
||||
|
mutate: vi.fn(), |
||||
|
mutateAsync: mockMutateAsync, |
||||
|
isPending: false, |
||||
|
isError: false, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/translations/provider", () => ({ |
||||
|
useI18n: () => ({ |
||||
|
locale: "en", |
||||
|
dictionary: { |
||||
|
upload_certificates: "upload certificates", |
||||
|
add_another_document: "Add another document", |
||||
|
max_files_reached: "Maximum of 4 files uploaded", |
||||
|
remove_document: "Remove document", |
||||
|
upload_failed: "Upload failed. Please try again.", |
||||
|
}, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("./question-answer-storage", () => ({ |
||||
|
useQuestionAnswers: () => ({ |
||||
|
getAnswerValue: (q: QuestionField) => answerMap[q.id] ?? null, |
||||
|
setAnswerValue: mockSetAnswerValue, |
||||
|
isLoading: false, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
const mockFileQuestion: QuestionField = { |
||||
|
id: "documents_verification.valid_identification_document", |
||||
|
title: "Valid Identification Document", |
||||
|
type: "file", |
||||
|
order: 1, |
||||
|
required: true, |
||||
|
baseRequired: true, |
||||
|
isVisible: true, |
||||
|
description: "Passport, National ID, or Driver's License", |
||||
|
tooltip: "", |
||||
|
extras: { |
||||
|
placeHolder: "Upload document", |
||||
|
range: [0, 0], |
||||
|
options: [".pdf", ".jpg", ".jpeg", ".png"], |
||||
|
}, |
||||
|
options: [], |
||||
|
}; |
||||
|
|
||||
|
describe("QuestionFile Component (Multiple Upload)", () => { |
||||
|
beforeEach(() => { |
||||
|
answerMap = {}; |
||||
|
mockSetAnswerValue.mockClear(); |
||||
|
mockMutateAsync.mockClear(); |
||||
|
vi.clearAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
cleanup(); |
||||
|
}); |
||||
|
|
||||
|
it("renders empty dropzone when no files are uploaded without formats text", () => { |
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText(/Valid Identification/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Passport, National ID, or Driver's License/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText("upload certificates")).toBeInTheDocument(); |
||||
|
expect(screen.queryByText("pdf, jpg, jpeg, png")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("renders legacy single file stored as string correctly", () => { |
||||
|
answerMap[mockFileQuestion.id] = "/media/marriage/media/passport.pdf"; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("passport.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Add another document/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/(1\/4)/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("renders multiple stored files correctly", () => { |
||||
|
answerMap[mockFileQuestion.id] = [ |
||||
|
"/media/marriage/media/doc1.pdf", |
||||
|
"/media/marriage/media/doc2.jpg", |
||||
|
"/media/marriage/media/doc3.png", |
||||
|
]; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("doc1.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByAltText("doc2.jpg")).toBeInTheDocument(); |
||||
|
expect(screen.getByAltText("doc3.png")).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Add another document/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/(3\/4)/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("uploads a new file and transitions from empty state to list with Add button", async () => { |
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
const input = screen.getByLabelText("Upload files"); |
||||
|
expect(input).toBeInTheDocument(); |
||||
|
|
||||
|
const file = new File(["dummy content"], "national_id.pdf", { |
||||
|
type: "application/pdf", |
||||
|
}); |
||||
|
|
||||
|
await act(async () => { |
||||
|
fireEvent.change(input, { target: { files: [file] } }); |
||||
|
}); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mockMutateAsync).toHaveBeenCalledWith(file); |
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockFileQuestion, [ |
||||
|
"/media/tmp/national_id.pdf", |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
expect(screen.getByText("national_id.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Add another document/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("allows adding 2nd, 3rd, 4th file and disables upload at 4 items", async () => { |
||||
|
answerMap[mockFileQuestion.id] = [ |
||||
|
"/media/marriage/media/file1.pdf", |
||||
|
"/media/marriage/media/file2.pdf", |
||||
|
"/media/marriage/media/file3.pdf", |
||||
|
]; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("file1.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("file2.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("file3.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/(3\/4)/)).toBeInTheDocument(); |
||||
|
|
||||
|
// Add 4th file
|
||||
|
const addInput = screen.getByLabelText("Add file"); |
||||
|
const fourthFile = new File(["content"], "file4.jpg", { type: "image/jpeg" }); |
||||
|
|
||||
|
await act(async () => { |
||||
|
fireEvent.change(addInput, { target: { files: [fourthFile] } }); |
||||
|
}); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockFileQuestion, [ |
||||
|
"/media/marriage/media/file1.pdf", |
||||
|
"/media/marriage/media/file2.pdf", |
||||
|
"/media/marriage/media/file3.pdf", |
||||
|
"/media/tmp/file4.jpg", |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
// Now 4 items are present, max files reached badge should be displayed
|
||||
|
expect(screen.getByText("Maximum of 4 files uploaded")).toBeInTheDocument(); |
||||
|
expect(screen.queryByText(/Add another document/)).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("deleting an item removes it from list, preserves remaining items, and restores Add button", async () => { |
||||
|
answerMap[mockFileQuestion.id] = [ |
||||
|
"/media/marriage/media/file1.pdf", |
||||
|
"/media/marriage/media/file2.jpg", |
||||
|
"/media/marriage/media/file3.png", |
||||
|
"/media/marriage/media/file4.pdf", |
||||
|
]; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("Maximum of 4 files uploaded")).toBeInTheDocument(); |
||||
|
|
||||
|
const deleteButtons = screen.getAllByTitle("Remove document"); |
||||
|
expect(deleteButtons).toHaveLength(4); |
||||
|
|
||||
|
// Delete 2nd file (file2.jpg)
|
||||
|
await act(async () => { |
||||
|
fireEvent.click(deleteButtons[1]); |
||||
|
}); |
||||
|
|
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockFileQuestion, [ |
||||
|
"/media/marriage/media/file1.pdf", |
||||
|
"/media/marriage/media/file3.png", |
||||
|
"/media/marriage/media/file4.pdf", |
||||
|
]); |
||||
|
|
||||
|
// Now 3 items remain, add button should be visible again
|
||||
|
expect(screen.getByText(/Add another document/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/(3\/4)/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("deleting the only item clears answer back to null and returns to empty dropzone", async () => { |
||||
|
answerMap[mockFileQuestion.id] = ["/media/marriage/media/only_file.pdf"]; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("only_file.pdf")).toBeInTheDocument(); |
||||
|
|
||||
|
const deleteButton = screen.getByTitle("Remove document"); |
||||
|
await act(async () => { |
||||
|
fireEvent.click(deleteButton); |
||||
|
}); |
||||
|
|
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockFileQuestion, null); |
||||
|
expect(screen.getByText("upload certificates")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue