Browse Source
feat: implement marriage outcome reporting flow with internationalization and testing support
staging
feat: implement marriage outcome reporting flow with internationalization and testing support
staging
35 changed files with 1323 additions and 619 deletions
-
3AGENTS.md
-
58src/app/candidate-contact/candidate-contact-client.tsx
-
342src/app/request-accepted/request-accepted-client.test.tsx
-
504src/app/request-accepted/request-accepted-client.tsx
-
28src/components/Componentes/female-outcome-sheet.test.tsx
-
66src/components/Componentes/female-outcome-sheet.tsx
-
22src/components/Componentes/navigation-button.tsx
-
27src/components/Componentes/support-access.ts
-
10src/components/Componentes/swipe-button.tsx
-
1src/components/Componentes/test-completed-sheet.tsx
-
5src/hooks/marriage/types.ts
-
26src/translations/locales/ar.json
-
22src/translations/locales/az.json
-
26src/translations/locales/bn.json
-
64src/translations/locales/da.json
-
62src/translations/locales/de.json
-
9src/translations/locales/en.json
-
62src/translations/locales/es.json
-
19src/translations/locales/fa.json
-
62src/translations/locales/fr.json
-
62src/translations/locales/gu.json
-
64src/translations/locales/ha.json
-
24src/translations/locales/he.json
-
62src/translations/locales/hi.json
-
24src/translations/locales/id.json
-
24src/translations/locales/ks.json
-
36src/translations/locales/pt.json
-
20src/translations/locales/ru.json
-
24src/translations/locales/sw.json
-
24src/translations/locales/tg.json
-
24src/translations/locales/tr.json
-
24src/translations/locales/ul.json
-
24src/translations/locales/ur.json
-
24src/translations/locales/uz.json
-
64src/translations/locales/zh.json
@ -0,0 +1,342 @@ |
|||||
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react"; |
||||
|
import userEvent from "@testing-library/user-event"; |
||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; |
||||
|
import { I18nProvider } from "@/translations/provider"; |
||||
|
import RequestAcceptedClient from "./request-accepted-client"; |
||||
|
|
||||
|
const mockReplace = vi.fn(); |
||||
|
const mockPush = vi.fn(); |
||||
|
const mockBack = vi.fn(); |
||||
|
|
||||
|
vi.mock("next/navigation", () => ({ |
||||
|
useRouter: () => ({ |
||||
|
push: mockPush, |
||||
|
replace: mockReplace, |
||||
|
back: mockBack, |
||||
|
prefetch: vi.fn(), |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
let mockProfileState = { |
||||
|
data: undefined as any, |
||||
|
isLoading: false, |
||||
|
isFetched: true, |
||||
|
isFetching: false, |
||||
|
}; |
||||
|
|
||||
|
vi.mock("@/hooks/marriage/use-profile-main", () => ({ |
||||
|
useMarriageProfileQuery: () => ({ |
||||
|
...mockProfileState, |
||||
|
refetch: vi.fn().mockResolvedValue({}), |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/marriage/use-contact-info", () => ({ |
||||
|
useMarriageContactInfoQuery: () => ({ |
||||
|
data: { contact_info: [] }, |
||||
|
isLoading: false, |
||||
|
refetch: vi.fn().mockResolvedValue({}), |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
const mockOutcomeMutateAsync = vi.fn(); |
||||
|
const mockContactStatusMutateAsync = vi.fn(); |
||||
|
|
||||
|
vi.mock("@/hooks/marriage/use-contact-status", () => ({ |
||||
|
useSubmitMarriageContactStatusMutation: () => ({ |
||||
|
mutateAsync: mockContactStatusMutateAsync, |
||||
|
isPending: false, |
||||
|
}), |
||||
|
useSubmitMarriageOutcomeMutation: () => ({ |
||||
|
mutateAsync: mockOutcomeMutateAsync, |
||||
|
isPending: false, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/marriage/use-marriage-advisors", () => ({ |
||||
|
useMarriageAdvisorsQuery: () => ({ |
||||
|
data: { results: [] }, |
||||
|
isLoading: false, |
||||
|
isError: false, |
||||
|
refetch: vi.fn(), |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/components/Componentes/marriage-advisors-overlay", () => ({ |
||||
|
useMarriageAdvisorsOverlay: () => ({ |
||||
|
isAdvisorOpen: false, |
||||
|
openAdvisors: vi.fn(), |
||||
|
closeAdvisors: vi.fn(), |
||||
|
}), |
||||
|
default: () => null, |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/components/Componentes/match-profile-overlay", () => ({ |
||||
|
useMatchProfileOverlay: () => ({ |
||||
|
isProfileOpen: false, |
||||
|
openProfile: vi.fn(), |
||||
|
closeProfile: vi.fn(), |
||||
|
}), |
||||
|
default: () => null, |
||||
|
})); |
||||
|
|
||||
|
function createWrapper() { |
||||
|
const queryClient = new QueryClient({ |
||||
|
defaultOptions: { |
||||
|
queries: { retry: false }, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return function Wrapper({ children }: { children: React.ReactNode }) { |
||||
|
return ( |
||||
|
<QueryClientProvider client={queryClient}> |
||||
|
<I18nProvider locale="en">{children}</I18nProvider> |
||||
|
</QueryClientProvider> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
describe("RequestAcceptedClient", () => { |
||||
|
beforeEach(() => { |
||||
|
mockReplace.mockReset(); |
||||
|
mockPush.mockReset(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
cleanup(); |
||||
|
vi.restoreAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
it("renders skeleton loading state and never renders obsolete 'Pay and get contact' or 'REQUEST APPROVED!' when profile is loading", () => { |
||||
|
mockProfileState = { |
||||
|
data: undefined, |
||||
|
isLoading: true, |
||||
|
isFetched: false, |
||||
|
isFetching: true, |
||||
|
}; |
||||
|
|
||||
|
const wrapper = createWrapper(); |
||||
|
render(<RequestAcceptedClient />, { wrapper }); |
||||
|
|
||||
|
expect(screen.queryByText(/pay and get contact/i)).not.toBeInTheDocument(); |
||||
|
expect(screen.queryByText(/request approved!/i)).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("renders 'Contact info released' and 'Contact' button for male candidate with payment_done", () => { |
||||
|
mockProfileState = { |
||||
|
data: { |
||||
|
id: 1, |
||||
|
gender: "male", |
||||
|
status: "in_case", |
||||
|
active_case: { |
||||
|
case_id: 101, |
||||
|
status: "payment_done", |
||||
|
can_report_no_contact: false, |
||||
|
}, |
||||
|
}, |
||||
|
isLoading: false, |
||||
|
isFetched: true, |
||||
|
isFetching: false, |
||||
|
}; |
||||
|
|
||||
|
const wrapper = createWrapper(); |
||||
|
render(<RequestAcceptedClient />, { wrapper }); |
||||
|
|
||||
|
expect(screen.getByText("Contact info released")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("Contact")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("View all detail")).toBeInTheDocument(); |
||||
|
expect(screen.queryByText(/pay and get contact/i)).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("renders 'Contact info released' with full-width 'View all detail' when contacted", () => { |
||||
|
mockProfileState = { |
||||
|
data: { |
||||
|
id: 1, |
||||
|
gender: "male", |
||||
|
status: "in_case", |
||||
|
active_case: { |
||||
|
case_id: 101, |
||||
|
status: "contacted", |
||||
|
can_report_no_contact: false, |
||||
|
}, |
||||
|
}, |
||||
|
isLoading: false, |
||||
|
isFetched: true, |
||||
|
isFetching: false, |
||||
|
}; |
||||
|
|
||||
|
const wrapper = createWrapper(); |
||||
|
render(<RequestAcceptedClient />, { wrapper }); |
||||
|
|
||||
|
expect(screen.getByText("Contact info released")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("View all detail")).toBeInTheDocument(); |
||||
|
expect(screen.queryByText("Contact")).not.toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText(/The 'Share Result' option will become active after 48 hours\./i), |
||||
|
).toBeInTheDocument(); |
||||
|
const shareResultBtn = screen.getByText("Share Result"); |
||||
|
expect(shareResultBtn).toBeInTheDocument(); |
||||
|
expect(shareResultBtn.closest("button")).toBeDisabled(); |
||||
|
expect(screen.queryByText(/pay and get contact/i)).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("enables 'Share Result' for male after 48h", () => { |
||||
|
mockProfileState = { |
||||
|
data: { |
||||
|
id: 1, |
||||
|
gender: "male", |
||||
|
status: "in_case", |
||||
|
active_case: { |
||||
|
case_id: 101, |
||||
|
status: "contacted", |
||||
|
can_report_no_contact: true, |
||||
|
can_report_outcome: true, |
||||
|
}, |
||||
|
}, |
||||
|
isLoading: false, |
||||
|
isFetched: true, |
||||
|
isFetching: false, |
||||
|
}; |
||||
|
|
||||
|
const wrapper = createWrapper(); |
||||
|
render(<RequestAcceptedClient />, { wrapper }); |
||||
|
|
||||
|
const shareResultBtn = screen.getByText("Share Result"); |
||||
|
expect(shareResultBtn).toBeInTheDocument(); |
||||
|
expect(shareResultBtn.closest("button")).not.toBeDisabled(); |
||||
|
expect( |
||||
|
screen.queryByText(/The 'Share Result' option will become active after 48 hours\./i), |
||||
|
).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("renders 'Share Result' and 'View all detail' for female candidate, and handles 'Continue' without mutations", async () => { |
||||
|
mockProfileState = { |
||||
|
data: { |
||||
|
id: 2, |
||||
|
gender: "female", |
||||
|
status: "in_case", |
||||
|
active_case: { |
||||
|
case_id: 202, |
||||
|
status: "contacted", |
||||
|
can_report_no_contact: false, |
||||
|
}, |
||||
|
}, |
||||
|
isLoading: false, |
||||
|
isFetched: true, |
||||
|
isFetching: false, |
||||
|
}; |
||||
|
|
||||
|
const wrapper = createWrapper(); |
||||
|
render(<RequestAcceptedClient />, { wrapper }); |
||||
|
|
||||
|
expect(screen.getByText(/request approved/i)).toBeInTheDocument(); |
||||
|
expect(screen.getByText("View all detail")).toBeInTheDocument(); |
||||
|
const shareResultBtn = screen.getByText("Share Result"); |
||||
|
expect(shareResultBtn).toBeInTheDocument(); |
||||
|
|
||||
|
// Open Share Result modal
|
||||
|
await userEvent.click(shareResultBtn); |
||||
|
|
||||
|
expect( |
||||
|
screen.getByRole("heading", { |
||||
|
name: "What was the outcome of your contact?", |
||||
|
}), |
||||
|
).toBeInTheDocument(); |
||||
|
|
||||
|
// Option 1: "Still getting to know each other" - clicking Continue simply closes modal
|
||||
|
const continueBtn = screen.getByRole("button", { name: "Continue" }); |
||||
|
await userEvent.click(continueBtn); |
||||
|
|
||||
|
expect(mockOutcomeMutateAsync).not.toHaveBeenCalled(); |
||||
|
expect( |
||||
|
screen.queryByRole("heading", { |
||||
|
name: "What was the outcome of your contact?", |
||||
|
}), |
||||
|
).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("executes decline flow for female: selects reason, confirms, calls mutation, and displays final conclusion message", async () => { |
||||
|
mockOutcomeMutateAsync.mockResolvedValueOnce({ success: true }); |
||||
|
|
||||
|
mockProfileState = { |
||||
|
data: { |
||||
|
id: 2, |
||||
|
gender: "female", |
||||
|
status: "in_case", |
||||
|
active_case: { |
||||
|
case_id: 202, |
||||
|
status: "contacted", |
||||
|
can_report_no_contact: false, |
||||
|
}, |
||||
|
}, |
||||
|
isLoading: false, |
||||
|
isFetched: true, |
||||
|
isFetching: false, |
||||
|
}; |
||||
|
|
||||
|
const wrapper = createWrapper(); |
||||
|
render(<RequestAcceptedClient />, { wrapper }); |
||||
|
|
||||
|
// Open Share Result modal
|
||||
|
await userEvent.click(screen.getByText("Share Result")); |
||||
|
|
||||
|
// Select decline option
|
||||
|
await userEvent.click( |
||||
|
screen.getByLabelText( |
||||
|
/We did not reach the agreement needed to continue acquaintance/i, |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
// Select a reason
|
||||
|
await userEvent.click( |
||||
|
screen.getByRole("button", { name: "No mutual interest" }), |
||||
|
); |
||||
|
|
||||
|
// Click Confirm
|
||||
|
const confirmBtn = screen.getByRole("button", { name: "Confirm" }); |
||||
|
await userEvent.click(confirmBtn); |
||||
|
|
||||
|
// Check mutation called with failure and selected reason
|
||||
|
expect(mockOutcomeMutateAsync).toHaveBeenCalledWith({ |
||||
|
status: "failure", |
||||
|
custom_note: "No mutual interest", |
||||
|
}); |
||||
|
|
||||
|
// Check final status rendered
|
||||
|
expect(screen.getByText(/acquaintance concluded/i)).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText( |
||||
|
"This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.", |
||||
|
), |
||||
|
).toBeInTheDocument(); |
||||
|
expect(screen.getByText("Contact Support")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("View all detail")).toBeInTheDocument(); |
||||
|
expect(screen.queryByText("Share Result")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("renders active Step 3 (Request Approved) for female candidate with female_accepted and does not show concluded even if sessionStorage had decline", () => { |
||||
|
sessionStorage.setItem("recent_declined_outcome", "true"); |
||||
|
|
||||
|
mockProfileState.data = { |
||||
|
id: 2, |
||||
|
gender: "female", |
||||
|
is_registering_for_self: true, |
||||
|
status: "in_case", |
||||
|
active_case: { |
||||
|
case_id: 44, |
||||
|
status: "female_accepted", |
||||
|
has_outcome_reported: false, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
render(<RequestAcceptedClient locale="en" />); |
||||
|
|
||||
|
expect(screen.getByText("Request Approved")).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText("The selected candidate will contact your family shortly."), |
||||
|
).toBeInTheDocument(); |
||||
|
expect(screen.queryByText(/acquaintance concluded/i)).not.toBeInTheDocument(); |
||||
|
expect(sessionStorage.getItem("recent_declined_outcome")).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue