Browse Source
feat: implement marriage matchmaking question flow and advisor action interface with multi-language support
Dev
feat: implement marriage matchmaking question flow and advisor action interface with multi-language support
Dev
13 changed files with 938 additions and 340 deletions
-
570src/app/new-match/new-match-client.tsx
-
55src/app/questions-list/[slug]/question-detail-client.tsx
-
58src/app/questions-list/questions-list-client.tsx
-
23src/app/questions-list/sections-request.tsx
-
24src/components/Componentes/advisor-actions-card.tsx
-
67src/components/Componentes/test-completed-sheet.test.tsx
-
61src/components/Componentes/test-completed-sheet.tsx
-
92src/components/Componentes/test-exit-sheet.test.tsx
-
78src/components/Componentes/test-exit-sheet.tsx
-
156src/components/Componentes/test-questions-flow.tsx
-
86src/lib/schema-adapter.ts
-
4src/translations/locales/en.json
-
4src/translations/locales/fa.json
@ -0,0 +1,67 @@ |
|||||
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react"; |
||||
|
import { afterEach, describe, expect, it, vi } from "vitest"; |
||||
|
import { TestCompletedSheet } from "./test-completed-sheet"; |
||||
|
|
||||
|
vi.mock("@/translations/provider", () => ({ |
||||
|
useI18n: () => ({ |
||||
|
locale: "fa", |
||||
|
dictionary: {}, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/use-hardware-back-handler", () => ({ |
||||
|
useHardwareBackHandler: vi.fn(), |
||||
|
})); |
||||
|
|
||||
|
describe("TestCompletedSheet", () => { |
||||
|
afterEach(() => { |
||||
|
cleanup(); |
||||
|
}); |
||||
|
|
||||
|
it("renders completion message and 'متوجه شدم' button when open", () => { |
||||
|
render( |
||||
|
<TestCompletedSheet |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
title="تست گلاسر" |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByRole("heading", { name: "تست گلاسر" })).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText( |
||||
|
"شما این آزمون را قبلاً تکمیل کردهاید و امکان شرکت مجدد در آن وجود ندارد.", |
||||
|
), |
||||
|
).toBeInTheDocument(); |
||||
|
expect(screen.getByRole("button", { name: "متوجه شدم" })).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("calls onClose when clicking 'متوجه شدم' button", async () => { |
||||
|
const handleClose = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<TestCompletedSheet |
||||
|
isOpen={true} |
||||
|
onClose={handleClose} |
||||
|
title="تست شخصیتشناسی" |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
const gotItBtn = screen.getByRole("button", { name: "متوجه شدم" }); |
||||
|
fireEvent.click(gotItBtn); |
||||
|
|
||||
|
await new Promise((r) => setTimeout(r, 260)); |
||||
|
expect(handleClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("returns null when isOpen is false", () => { |
||||
|
const { container } = render( |
||||
|
<TestCompletedSheet |
||||
|
isOpen={false} |
||||
|
onClose={vi.fn()} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
expect(container.firstChild).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,61 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useI18n } from "@/translations/provider"; |
||||
|
import InformationSheet from "./information-sheet"; |
||||
|
import SwipeButton from "./swipe-button"; |
||||
|
|
||||
|
export type TestCompletedSheetProps = { |
||||
|
isOpen: boolean; |
||||
|
onClose: () => void; |
||||
|
title?: string; |
||||
|
closeOnOutside?: boolean; |
||||
|
}; |
||||
|
|
||||
|
export function TestCompletedSheet({ |
||||
|
isOpen, |
||||
|
onClose, |
||||
|
title, |
||||
|
closeOnOutside = true, |
||||
|
}: TestCompletedSheetProps) { |
||||
|
const { dictionary: t, locale } = useI18n(); |
||||
|
|
||||
|
if (!isOpen) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const isFa = locale === "fa"; |
||||
|
|
||||
|
const sheetTitle = |
||||
|
title || |
||||
|
(t as Record<string, string>)["Test Completed"] || |
||||
|
(isFa ? "آزمون تکمیل شده است" : "Test Completed"); |
||||
|
|
||||
|
const message = isFa |
||||
|
? "شما این آزمون را قبلاً تکمیل کردهاید و امکان شرکت مجدد در آن وجود ندارد." |
||||
|
: (t as Record<string, string>)["You have already completed this test, and it cannot be retaken."] || |
||||
|
"You have already completed this test, and it cannot be retaken."; |
||||
|
|
||||
|
const buttonLabel = (t as Record<string, string>)["Got it"] || (isFa ? "متوجه شدم" : "Got it"); |
||||
|
|
||||
|
return ( |
||||
|
<InformationSheet |
||||
|
icon="check" |
||||
|
title={sheetTitle} |
||||
|
description={ |
||||
|
<p className="text-center mt-2 group-12 text-[#4D4D4D] leading-relaxed font-medium"> |
||||
|
{message} |
||||
|
</p> |
||||
|
} |
||||
|
buttons={({ close }) => ( |
||||
|
<SwipeButton |
||||
|
text={buttonLabel} |
||||
|
onSuccess={close} |
||||
|
/> |
||||
|
)} |
||||
|
closeOnOutside={closeOnOutside} |
||||
|
onClose={onClose} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default TestCompletedSheet; |
||||
@ -0,0 +1,92 @@ |
|||||
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react"; |
||||
|
import { afterEach, describe, expect, it, vi } from "vitest"; |
||||
|
import { TestExitSheet } from "./test-exit-sheet"; |
||||
|
|
||||
|
vi.mock("@/translations/provider", () => ({ |
||||
|
useI18n: () => ({ |
||||
|
locale: "fa", |
||||
|
dictionary: {}, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/use-hardware-back-handler", () => ({ |
||||
|
useHardwareBackHandler: vi.fn(), |
||||
|
})); |
||||
|
|
||||
|
describe("TestExitSheet", () => { |
||||
|
afterEach(() => { |
||||
|
cleanup(); |
||||
|
}); |
||||
|
|
||||
|
it("renders warning title and explanation points when open", () => { |
||||
|
render( |
||||
|
<TestExitSheet |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onConfirmExit={vi.fn()} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByRole("heading", { name: "خروج از آزمون" })).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText("در صورت خروج از تست، ادامه فعلی حفظ نمیشود."), |
||||
|
).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText("پاسخهای واردشده ذخیره نخواهند شد."), |
||||
|
).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText("برای انجام دوباره تست باید از ابتدا شروع کنید."), |
||||
|
).toBeInTheDocument(); |
||||
|
expect(screen.getByText("ادامه آزمون")).toBeInTheDocument(); |
||||
|
expect(screen.getByRole("button", { name: "خروج از آزمون" })).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("calls onClose when clicking continue test (cancel)", async () => { |
||||
|
const handleClose = vi.fn(); |
||||
|
const handleConfirm = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<TestExitSheet |
||||
|
isOpen={true} |
||||
|
onClose={handleClose} |
||||
|
onConfirmExit={handleConfirm} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
const continueBtn = screen.getByText("ادامه آزمون"); |
||||
|
fireEvent.click(continueBtn); |
||||
|
|
||||
|
await new Promise((r) => setTimeout(r, 260)); |
||||
|
expect(handleClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("calls onConfirmExit when confirming exit", () => { |
||||
|
const handleClose = vi.fn(); |
||||
|
const handleConfirm = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<TestExitSheet |
||||
|
isOpen={true} |
||||
|
onClose={handleClose} |
||||
|
onConfirmExit={handleConfirm} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
const exitBtn = screen.getByRole("button", { name: "خروج از آزمون" }); |
||||
|
fireEvent.click(exitBtn); |
||||
|
|
||||
|
expect(handleConfirm).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("returns null when isOpen is false", () => { |
||||
|
const { container } = render( |
||||
|
<TestExitSheet |
||||
|
isOpen={false} |
||||
|
onClose={vi.fn()} |
||||
|
onConfirmExit={vi.fn()} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
expect(container.firstChild).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,78 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useI18n } from "@/translations/provider"; |
||||
|
import InformationSheet from "./information-sheet"; |
||||
|
import SwipeButton from "./swipe-button"; |
||||
|
|
||||
|
export type TestExitSheetProps = { |
||||
|
isOpen: boolean; |
||||
|
onClose: () => void; |
||||
|
onConfirmExit: () => void; |
||||
|
closeOnOutside?: boolean; |
||||
|
}; |
||||
|
|
||||
|
export function TestExitSheet({ |
||||
|
isOpen, |
||||
|
onClose, |
||||
|
onConfirmExit, |
||||
|
closeOnOutside = true, |
||||
|
}: TestExitSheetProps) { |
||||
|
const { dictionary: t, locale } = useI18n(); |
||||
|
|
||||
|
if (!isOpen) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const isFa = locale === "fa"; |
||||
|
|
||||
|
const tr = t as Record<string, string>; |
||||
|
const title = tr["Exit Test"] || (isFa ? "خروج از آزمون" : "Exit Test"); |
||||
|
|
||||
|
const points = isFa |
||||
|
? [ |
||||
|
"در صورت خروج از تست، ادامه فعلی حفظ نمیشود.", |
||||
|
"پاسخهای واردشده ذخیره نخواهند شد.", |
||||
|
"برای انجام دوباره تست باید از ابتدا شروع کنید.", |
||||
|
] |
||||
|
: [ |
||||
|
tr["If you exit the test, your current progress will not be saved."] || |
||||
|
"If you exit the test, your current progress will not be saved.", |
||||
|
tr["Your entered answers will not be saved."] || |
||||
|
"Your entered answers will not be saved.", |
||||
|
tr["To take the test again, you must start from the beginning."] || |
||||
|
"To take the test again, you must start from the beginning.", |
||||
|
]; |
||||
|
|
||||
|
const cancelLabel = |
||||
|
tr["Continue Test"] || (isFa ? "ادامه آزمون" : "Continue Test"); |
||||
|
const exitLabel = |
||||
|
tr["Exit Test"] || (isFa ? "خروج از آزمون" : "Exit Test"); |
||||
|
|
||||
|
return ( |
||||
|
<InformationSheet |
||||
|
icon="warning" |
||||
|
title={title} |
||||
|
description={ |
||||
|
<div className="flex flex-col gap-2 text-center mt-2 group-12 text-[#4D4D4D] leading-relaxed"> |
||||
|
{points.map((pt, idx) => ( |
||||
|
<p key={idx} className="font-medium"> |
||||
|
{pt} |
||||
|
</p> |
||||
|
))} |
||||
|
</div> |
||||
|
} |
||||
|
buttons={({ close }) => ( |
||||
|
<SwipeButton |
||||
|
text={exitLabel} |
||||
|
cancelText={cancelLabel} |
||||
|
onCancel={close} |
||||
|
onSuccess={onConfirmExit} |
||||
|
/> |
||||
|
)} |
||||
|
closeOnOutside={closeOnOutside} |
||||
|
onClose={onClose} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default TestExitSheet; |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue