import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { resetCloseOnHistoryBackForTests, useCloseOnHistoryBack } from '@/hooks/useCloseOnHistoryBack' /** Browser has already moved history before `popstate` — dispatch only. */ const pressBrowserBack = () => { act(() => { window.dispatchEvent(new PopStateEvent('popstate', { state: window.history.state })) }) } describe('useCloseOnHistoryBack', () => { let pushStateSpy: ReturnType let backSpy: ReturnType beforeEach(() => { resetCloseOnHistoryBackForTests() pushStateSpy = vi.spyOn(window.history, 'pushState') // jsdom often delays or skips popstate for history.back(); keep ignorePopCount in sync. backSpy = vi.spyOn(window.history, 'back').mockImplementation(() => { window.dispatchEvent(new PopStateEvent('popstate', { state: window.history.state })) }) }) afterEach(() => { resetCloseOnHistoryBackForTests() pushStateSpy.mockRestore() backSpy.mockRestore() vi.restoreAllMocks() }) it('pushes a history entry when the overlay opens', () => { const onRequestClose = vi.fn() renderHook(() => { useCloseOnHistoryBack({ isOpen: true, onRequestClose, }) }) expect(pushStateSpy).toHaveBeenCalledWith(expect.objectContaining({ consumerModalHistory: expect.any(Number) }), '') expect(onRequestClose).not.toHaveBeenCalled() }) it('does not push when enabled is false', () => { renderHook(() => { useCloseOnHistoryBack({ isOpen: true, enabled: false, onRequestClose: vi.fn(), }) }) expect(pushStateSpy).not.toHaveBeenCalled() }) it('closes on browser Back without calling history.back again', () => { const onRequestClose = vi.fn() renderHook(() => { useCloseOnHistoryBack({ isOpen: true, onRequestClose, }) }) backSpy.mockClear() pressBrowserBack() expect(onRequestClose).toHaveBeenCalledOnce() expect(backSpy).not.toHaveBeenCalled() }) it('consumes the history entry on programmatic close', () => { const onRequestClose = vi.fn() const { rerender } = renderHook( ({ isOpen }) => { useCloseOnHistoryBack({ isOpen, onRequestClose, }) }, { initialProps: { isOpen: true } } ) backSpy.mockClear() rerender({ isOpen: false }) expect(backSpy).toHaveBeenCalledOnce() expect(onRequestClose).not.toHaveBeenCalled() }) it('traps Back when allowClose is false by re-pushing history', () => { const onRequestClose = vi.fn() renderHook(() => { useCloseOnHistoryBack({ isOpen: true, allowClose: false, onRequestClose, }) }) const pushesAfterOpen = pushStateSpy.mock.calls.length pressBrowserBack() expect(onRequestClose).not.toHaveBeenCalled() expect(pushStateSpy.mock.calls.length).toBeGreaterThan(pushesAfterOpen) }) it('closes only the topmost nested overlay on Back', () => { const closeParent = vi.fn() const closeChild = vi.fn() renderHook(() => { useCloseOnHistoryBack({ isOpen: true, onRequestClose: closeParent }) useCloseOnHistoryBack({ isOpen: true, onRequestClose: closeChild }) }) pressBrowserBack() expect(closeChild).toHaveBeenCalledOnce() expect(closeParent).not.toHaveBeenCalled() pressBrowserBack() expect(closeParent).toHaveBeenCalledOnce() }) it('does not leave an orphan entry when the top nested overlay closes programmatically', () => { const closeParent = vi.fn() const closeChild = vi.fn() const { rerender } = renderHook( ({ childOpen }) => { useCloseOnHistoryBack({ isOpen: true, onRequestClose: closeParent }) useCloseOnHistoryBack({ isOpen: childOpen, onRequestClose: closeChild }) }, { initialProps: { childOpen: true } } ) backSpy.mockClear() rerender({ childOpen: false }) expect(backSpy).toHaveBeenCalledOnce() expect(closeChild).not.toHaveBeenCalled() expect(closeParent).not.toHaveBeenCalled() pressBrowserBack() expect(closeParent).toHaveBeenCalledOnce() }) })