admin/hooks/useCloseOnHistoryBack.test.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

160 lines
4.2 KiB
TypeScript

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<typeof vi.spyOn>
let backSpy: ReturnType<typeof vi.spyOn>
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()
})
})