diff --git a/src/app/finding-match/finding-match-client.test.tsx b/src/app/finding-match/finding-match-client.test.tsx new file mode 100644 index 0000000..99b783e --- /dev/null +++ b/src/app/finding-match/finding-match-client.test.tsx @@ -0,0 +1,181 @@ +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { I18nProvider } from "@/translations/provider"; +import FindingMatchClient from "./finding-match-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 mockProfileData: any = { + id: 1, + gender: "male", + status: "search_in_progress", + can_edit_profile: true, + unseen_rejection: null, +}; + +vi.mock("@/hooks/marriage/use-profile-main", () => ({ + useMarriageProfileQuery: () => ({ + data: mockProfileData, + isLoading: false, + isFetched: true, + refetch: vi.fn(), + }), +})); + +vi.mock("@/hooks/marriage/use-form-schema", () => ({ + useFormOverviewQuery: () => ({ + data: { + sections: [ + { + slug: "personal_identity", + title: "Personal Identity", + required: true, + estimated_time_min: 3, + is_completed: false, + progress: { + current_step: 0, + completion_percent: 0, + is_completed: false, + }, + }, + ], + }, + isLoading: false, + isFetched: true, + isError: false, + refetch: vi.fn(), + }), + getFormSection: vi.fn(), +})); + +vi.mock("@/hooks/marriage/use-cattell", () => ({ + useCattellResultQuery: () => ({ data: null }), + getCattellQuestions: vi.fn(), +})); + +vi.mock("@/hooks/marriage/use-glasser", () => ({ + useGlasserResultQuery: () => ({ data: null }), + getGlasserQuestions: vi.fn(), +})); + +vi.mock("@/hooks/marriage/use-match-start", () => ({ + useStartMarriageMatchMutation: () => ({ + mutate: vi.fn(), + isPending: false, + isError: false, + reset: vi.fn(), + }), +})); + +vi.mock("@/hooks/marriage/use-rejection-seen", () => ({ + useRejectionSeenMutation: () => ({ + mutate: vi.fn(), + isPending: false, + }), +})); + +vi.mock("@/hooks/marriage/use-marriage-advisors", () => ({ + useMarriageAdvisorsQuery: () => ({ + data: { results: [] }, + isLoading: false, + isError: false, + refetch: vi.fn(), + }), +})); + +function createWrapper() { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }); + + return function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + }; +} + +describe("FindingMatchClient", () => { + beforeEach(() => { + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => { + return setTimeout(() => cb(Date.now()), 0); + }); + vi.stubGlobal("cancelAnimationFrame", (id: number) => { + clearTimeout(id); + }); + document.body.className = ""; + mockProfileData = { + id: 1, + gender: "male", + status: "search_in_progress", + can_edit_profile: true, + unseen_rejection: null, + }; + }); + + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + vi.useRealTimers(); + document.body.className = ""; + }); + + it("renders search in progress and Edit Profile button", () => { + const wrapper = createWrapper(); + render(, { wrapper }); + + expect(screen.getByText("SEARCH IN PROGRESS")).toBeInTheDocument(); + expect(screen.getByText("Edit Profile")).toBeInTheDocument(); + }); + + it("opens questions list overlay when Edit Profile button is clicked", () => { + const wrapper = createWrapper(); + const { container } = render(, { wrapper }); + + // Initial render - overlay is pre-mounted offscreen + const editBtn = screen.getByRole("button", { name: /edit profile/i }); + expect(editBtn).toBeInTheDocument(); + + fireEvent.click(editBtn); + + act(() => { + vi.advanceTimersByTime(16); + }); + + const overlay = container.querySelector('aside[data-slot="section-overlay"][data-state="open"]'); + expect(overlay).toBeInTheDocument(); + expect(document.body.classList.contains("section-overlay-open")).toBe(true); + }); + + it("renders locked state when can_edit_profile is false", () => { + mockProfileData = { + id: 1, + gender: "male", + status: "search_in_progress", + can_edit_profile: false, + }; + + const wrapper = createWrapper(); + render(, { wrapper }); + + expect(screen.getByText("Profile is locked")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /edit profile/i })).not.toBeInTheDocument(); + }); +}); diff --git a/src/app/finding-match/finding-match-client.tsx b/src/app/finding-match/finding-match-client.tsx index 4b44acb..bbc7f6a 100644 --- a/src/app/finding-match/finding-match-client.tsx +++ b/src/app/finding-match/finding-match-client.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { useHabibWebReady } from "@/hooks/use-habib-web-ready"; import { Ic } from "@/icons"; @@ -10,6 +10,9 @@ import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import MarriageAdvisorsOverlay, { useMarriageAdvisorsOverlay, } from "@/components/Componentes/marriage-advisors-overlay"; +import QuestionsListOverlay, { + useQuestionsListOverlay, +} from "@/components/Componentes/questions-list-overlay"; import Button from "@/components/Componentes/button"; import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end"; import { PageBackground } from "@/components/Componentes/page-background"; @@ -31,6 +34,40 @@ export default function FindingMatchClient() { const { dictionary: t, locale } = useI18n(); const { isAdvisorOpen, openAdvisors, closeAdvisors } = useMarriageAdvisorsOverlay(); + const { isQuestionsOpen, openQuestions, closeQuestions } = + useQuestionsListOverlay(); + const [isQuestionsReady, setIsQuestionsReady] = useState(false); + const [isOpeningEditProfile, setIsOpeningEditProfile] = useState(false); + + const handleQuestionsReady = useCallback(() => { + setIsQuestionsReady(true); + }, []); + + useEffect(() => { + if (isQuestionsReady && isOpeningEditProfile) { + setIsOpeningEditProfile(false); + openQuestions(); + } + }, [isQuestionsReady, isOpeningEditProfile, openQuestions]); + + // Safety fallback if background preparation takes unexpectedly long + useEffect(() => { + if (!isOpeningEditProfile) return; + const timer = setTimeout(() => { + setIsOpeningEditProfile(false); + openQuestions(); + }, 2000); + return () => clearTimeout(timer); + }, [isOpeningEditProfile, openQuestions]); + + const handleEditProfileClick = useCallback(() => { + if (isQuestionsReady) { + openQuestions(); + } else { + setIsOpeningEditProfile(true); + } + }, [isQuestionsReady, openQuestions]); + const { data: profile, isLoading, isFetched } = useMarriageProfileQuery({ refetchInterval: 3000, }); @@ -43,6 +80,10 @@ export default function FindingMatchClient() { closeAdvisors(); return true; } + if (isQuestionsOpen) { + closeQuestions(); + return true; + } return false; // Hardware back closes the service in Flutter }); @@ -211,7 +252,8 @@ export default function FindingMatchClient() { ) : (