Browse Source
feat: implement MarriageAdvisorsOverlay with history synchronization and integrate into client pages
master
feat: implement MarriageAdvisorsOverlay with history synchronization and integrate into client pages
master
9 changed files with 277 additions and 8 deletions
-
12src/app/candidate-contact/candidate-contact-client.tsx
-
12src/app/finding-match/finding-match-client.tsx
-
7src/app/marriage-advisors/page.tsx
-
12src/app/new-match/new-match-client.tsx
-
12src/app/request-accepted/request-accepted-client.tsx
-
12src/app/request-sent/request-sent-client.tsx
-
7src/components/Componentes/advisor-actions-card.tsx
-
134src/components/Componentes/marriage-advisors-overlay.test.tsx
-
77src/components/Componentes/marriage-advisors-overlay.tsx
@ -0,0 +1,134 @@ |
|||
import { act, cleanup, render, renderHook, 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 { |
|||
MarriageAdvisorsOverlay, |
|||
useMarriageAdvisorsOverlay, |
|||
} from "./marriage-advisors-overlay"; |
|||
|
|||
vi.mock("next/navigation", () => ({ |
|||
useRouter: () => ({ |
|||
push: vi.fn(), |
|||
replace: vi.fn(), |
|||
back: vi.fn(), |
|||
}), |
|||
})); |
|||
|
|||
vi.mock("@/hooks/marriage/use-marriage-advisors", () => ({ |
|||
useMarriageAdvisorsQuery: () => ({ |
|||
data: { results: [] }, |
|||
isLoading: false, |
|||
isError: false, |
|||
refetch: vi.fn(), |
|||
}), |
|||
})); |
|||
|
|||
vi.mock("@/hooks/marriage/use-profile-main", () => ({ |
|||
useMarriageProfileQuery: () => ({ |
|||
data: null, |
|||
isLoading: false, |
|||
refetch: vi.fn(), |
|||
}), |
|||
})); |
|||
|
|||
function createWrapper() { |
|||
const queryClient = new QueryClient({ |
|||
defaultOptions: { |
|||
queries: { retry: false }, |
|||
}, |
|||
}); |
|||
|
|||
return function Wrapper({ children }: { children: React.ReactNode }) { |
|||
return ( |
|||
<QueryClientProvider client={queryClient}> |
|||
<I18nProvider locale="fa">{children}</I18nProvider> |
|||
</QueryClientProvider> |
|||
); |
|||
}; |
|||
} |
|||
|
|||
describe("MarriageAdvisorsOverlay & useMarriageAdvisorsOverlay", () => { |
|||
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 = ""; |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
cleanup(); |
|||
vi.restoreAllMocks(); |
|||
vi.useRealTimers(); |
|||
document.body.className = ""; |
|||
}); |
|||
|
|||
it("hook manages open and close state with history sync", () => { |
|||
const { result } = renderHook(() => useMarriageAdvisorsOverlay()); |
|||
|
|||
expect(result.current.isAdvisorOpen).toBe(false); |
|||
|
|||
act(() => { |
|||
result.current.openAdvisors(); |
|||
}); |
|||
|
|||
expect(result.current.isAdvisorOpen).toBe(true); |
|||
|
|||
act(() => { |
|||
result.current.closeAdvisors(); |
|||
}); |
|||
|
|||
expect(result.current.isAdvisorOpen).toBe(false); |
|||
}); |
|||
|
|||
it("renders overlay dialog and advisors page when open is true", () => { |
|||
const handleClose = vi.fn(); |
|||
const wrapper = createWrapper(); |
|||
|
|||
render( |
|||
<MarriageAdvisorsOverlay open={true} onClose={handleClose} />, |
|||
{ wrapper }, |
|||
); |
|||
|
|||
act(() => { |
|||
vi.advanceTimersByTime(16); |
|||
}); |
|||
|
|||
const overlay = screen.getByRole("dialog"); |
|||
expect(overlay).toBeInTheDocument(); |
|||
expect(overlay).toHaveClass("section-overlay"); |
|||
expect(document.body.classList.contains("section-overlay-open")).toBe(true); |
|||
}); |
|||
|
|||
it("handles closing animation and removes body class when open becomes false", () => { |
|||
const handleClose = vi.fn(); |
|||
const wrapper = createWrapper(); |
|||
|
|||
const { rerender } = render( |
|||
<MarriageAdvisorsOverlay open={true} onClose={handleClose} />, |
|||
{ wrapper }, |
|||
); |
|||
|
|||
act(() => { |
|||
vi.advanceTimersByTime(16); |
|||
}); |
|||
|
|||
expect(screen.getByRole("dialog")).toHaveAttribute("data-state", "open"); |
|||
expect(document.body.classList.contains("section-overlay-open")).toBe(true); |
|||
|
|||
rerender(<MarriageAdvisorsOverlay open={false} onClose={handleClose} />); |
|||
|
|||
expect(screen.getByRole("dialog")).toHaveAttribute("data-state", "closing"); |
|||
expect(document.body.classList.contains("section-overlay-open")).toBe(false); |
|||
|
|||
act(() => { |
|||
vi.advanceTimersByTime(210); |
|||
}); |
|||
|
|||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,77 @@ |
|||
"use client"; |
|||
|
|||
import { useCallback, useEffect, useState } from "react"; |
|||
import MarriageAdvisorsPage from "@/app/marriage-advisors/page"; |
|||
import SectionOverlayHost from "@/components/Componentes/section-overlay-host"; |
|||
import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; |
|||
|
|||
export type MarriageAdvisorsOverlayProps = { |
|||
open: boolean; |
|||
onClose: () => void; |
|||
}; |
|||
|
|||
/** |
|||
* Hook to manage Marriage Advisors slide-in overlay state. |
|||
* Syncs with browser history (?advisors=open) and intercepts hardware back in Flutter. |
|||
*/ |
|||
export function useMarriageAdvisorsOverlay() { |
|||
const [isAdvisorOpen, setIsAdvisorOpen] = useState(false); |
|||
|
|||
useEffect(() => { |
|||
const readAdvisorsFromUrl = () => { |
|||
if (typeof window === "undefined") return; |
|||
const params = new URLSearchParams(window.location.search); |
|||
setIsAdvisorOpen(params.get("advisors") === "open"); |
|||
}; |
|||
|
|||
readAdvisorsFromUrl(); |
|||
window.addEventListener("popstate", readAdvisorsFromUrl); |
|||
return () => window.removeEventListener("popstate", readAdvisorsFromUrl); |
|||
}, []); |
|||
|
|||
const openAdvisors = useCallback(() => { |
|||
setIsAdvisorOpen(true); |
|||
if (typeof window !== "undefined") { |
|||
const url = new URL(window.location.href); |
|||
url.searchParams.set("advisors", "open"); |
|||
window.history.pushState({ advisors: "open" }, "", url.toString()); |
|||
} |
|||
}, []); |
|||
|
|||
const closeAdvisors = useCallback(() => { |
|||
if (typeof window !== "undefined") { |
|||
const params = new URLSearchParams(window.location.search); |
|||
if (params.get("advisors") === "open") { |
|||
setIsAdvisorOpen(false); |
|||
window.history.back(); |
|||
return; |
|||
} |
|||
} |
|||
setIsAdvisorOpen(false); |
|||
}, []); |
|||
|
|||
// Intercept hardware back in Flutter WebView when advisor overlay is open
|
|||
useHardwareBackHandler(() => { |
|||
closeAdvisors(); |
|||
return true; // handled: keep WebView screen open
|
|||
}, isAdvisorOpen); |
|||
|
|||
return { |
|||
isAdvisorOpen, |
|||
openAdvisors, |
|||
closeAdvisors, |
|||
}; |
|||
} |
|||
|
|||
export function MarriageAdvisorsOverlay({ |
|||
open, |
|||
onClose, |
|||
}: MarriageAdvisorsOverlayProps) { |
|||
return ( |
|||
<SectionOverlayHost open={open} onClose={onClose}> |
|||
<MarriageAdvisorsPage onClose={onClose} /> |
|||
</SectionOverlayHost> |
|||
); |
|||
} |
|||
|
|||
export default MarriageAdvisorsOverlay; |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue