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