129 lines
6.4 KiB
TypeScript
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()
|
|
})
|