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