gongxiangp2/components/detail-generator/useUnsavedDetailGuard.test.tsx

129 lines
6.4 KiB
TypeScript

import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { useUnsavedDetailGuard } from './useUnsavedDetailGuard'
const mockPush = jest.fn()
const mockRouter = { push: mockPush }
jest.mock('next/navigation', () => ({ useRouter: () => mockRouter }))
function Fixture({ dirty, onNavigation = jest.fn(), href = '/workspaces/dashboard/core-metrics', download = false }: {
dirty: boolean; onNavigation?: () => void; href?: string; download?: boolean
}) {
const dialog = useUnsavedDetailGuard(dirty)
return <><input aria-label="未保存的文案" defaultValue="保留我的修改" /><a href={href} download={download || undefined} onClick={(event) => { event.preventDefault(); onNavigation() }}>侧栏入口</a>{dialog}</>
}
beforeEach(() => {
window.history.replaceState(null, '', '/assets/material-library')
window.history.pushState(null, '', '/assets/detail-generator?project=current')
jest.clearAllMocks()
jest.spyOn(window.history, 'back').mockImplementation(() => undefined)
jest.spyOn(window.history, 'forward').mockImplementation(() => undefined)
jest.spyOn(window.history, 'go').mockImplementation(() => undefined)
})
afterEach(() => jest.restoreAllMocks())
test('新标签没有上一页时不人为创建可返回的同页历史', () => {
jest.spyOn(window.history, 'length', 'get').mockReturnValue(1)
const push = jest.spyOn(window.history, 'pushState')
render(<Fixture dirty />)
expect(push).not.toHaveBeenCalled()
})
test('有未保存修改时刷新事件被保护,保存后正常放行', () => {
const { rerender } = render(<Fixture dirty />)
const unsavedRefresh = new Event('beforeunload', { cancelable: true })
window.dispatchEvent(unsavedRefresh)
expect(unsavedRefresh.defaultPrevented).toBe(true)
rerender(<Fixture dirty={false} />)
const savedRefresh = new Event('beforeunload', { cancelable: true })
window.dispatchEvent(savedRefresh)
expect(savedRefresh.defaultPrevented).toBe(false)
})
test('取消内部跳转保留草稿与当前位置', async () => {
const onNavigation = jest.fn()
render(<Fixture dirty onNavigation={onNavigation} />)
fireEvent.click(screen.getByRole('link', { name: '侧栏入口' }))
expect(await screen.findByRole('alertdialog', { name: '离开此页面?' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '继续编辑' }))
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument())
expect(screen.getByLabelText('未保存的文案')).toHaveValue('保留我的修改')
expect(window.location.pathname).toBe('/assets/detail-generator')
expect(onNavigation).not.toHaveBeenCalled()
expect(mockPush).not.toHaveBeenCalled()
})
test('确认放弃修改后允许内部跳转,不再重复触发刷新提示', async () => {
render(<Fixture dirty />)
fireEvent.click(screen.getByRole('link', { name: '侧栏入口' }))
fireEvent.click(await screen.findByRole('button', { name: '放弃修改并离开' }))
await waitFor(() => expect(window.history.back).toHaveBeenCalled())
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
await waitFor(() => expect(mockPush).toHaveBeenCalledWith('/workspaces/dashboard/core-metrics'))
const refresh = new Event('beforeunload', { cancelable: true })
window.dispatchEvent(refresh)
expect(refresh.defaultPrevented).toBe(false)
})
test('未保存时 Back 恢复守卫项,取消保留草稿且不重复添加历史', async () => {
const push = jest.spyOn(window.history, 'pushState')
render(<Fixture dirty />)
const sentinel = window.history.state
expect(push).toHaveBeenCalledTimes(1)
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
expect(window.history.forward).toHaveBeenCalledTimes(1)
act(() => { window.dispatchEvent(new PopStateEvent('popstate', { state: sentinel })) })
fireEvent.click(await screen.findByRole('button', { name: '继续编辑' }))
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument())
expect(screen.getByLabelText('未保存的文案')).toHaveValue('保留我的修改')
expect(push).toHaveBeenCalledTimes(1)
expect(window.history.go).not.toHaveBeenCalled()
})
test('Back 确认后跳过唯一守卫项,真实离开事件正常传给路由', async () => {
render(<Fixture dirty />)
const sentinel = window.history.state
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
act(() => { window.dispatchEvent(new PopStateEvent('popstate', { state: sentinel })) })
fireEvent.click(await screen.findByRole('button', { name: '放弃修改并离开' }))
await waitFor(() => expect(window.history.go).toHaveBeenCalledWith(-2))
const routed = jest.fn()
window.addEventListener('popstate', routed)
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
expect(routed).toHaveBeenCalledTimes(1)
window.removeEventListener('popstate', routed)
})
test('已保存的 Back 不恢复、不弹窗、不吞路由事件', () => {
render(<Fixture dirty={false} />)
const routed = jest.fn()
window.addEventListener('popstate', routed)
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
expect(routed).toHaveBeenCalledTimes(1)
expect(window.history.forward).not.toHaveBeenCalled()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
window.removeEventListener('popstate', routed)
})
test('已保存状态通过原链接正常导航', () => {
const onNavigation = jest.fn()
render(<Fixture dirty={false} onNavigation={onNavigation} />)
fireEvent.click(screen.getByRole('link', { name: '侧栏入口' }))
expect(onNavigation).toHaveBeenCalledTimes(1)
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
test.each([
{ href: '#screen-a', download: false, ctrlKey: false },
{ href: '/api/detail-generator/assets/export-file', download: false, ctrlKey: false },
{ href: '/assets/detail-generator?project=current', download: false, ctrlKey: false },
{ href: '/download-output', download: true, ctrlKey: false },
{ href: '/workspaces/dashboard/core-metrics', download: false, ctrlKey: true },
])('非离开操作不弹确认:$href', ({ href, download, ctrlKey }) => {
const onNavigation = jest.fn()
render(<Fixture dirty href={href} download={download} onNavigation={onNavigation} />)
fireEvent.click(screen.getByRole('link'), { ctrlKey })
expect(onNavigation).toHaveBeenCalledTimes(1)
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})