172 lines
6.1 KiB
TypeScript
172 lines
6.1 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { renderHook, act } from '@testing-library/react'
|
|
import { useNavigationHistory } from './useNavigationHistory'
|
|
|
|
function renderNavigationHistory(paths = []) {
|
|
const hook = renderHook(() => useNavigationHistory())
|
|
if (paths.length > 0) {
|
|
act(() => {
|
|
paths.forEach((path) => hook.result.current.push(path))
|
|
})
|
|
}
|
|
return hook
|
|
}
|
|
|
|
function navigate(result, direction, isValidPath) {
|
|
let target = null
|
|
act(() => { target = result.current[direction](isValidPath) })
|
|
return target
|
|
}
|
|
|
|
describe('useNavigationHistory', () => {
|
|
it('starts with empty state', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
expect(result.current.canGoBack).toBe(false)
|
|
expect(result.current.canGoForward).toBe(false)
|
|
})
|
|
|
|
it('push adds path to history', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => result.current.push('/a'))
|
|
expect(result.current.canGoBack).toBe(false)
|
|
expect(result.current.canGoForward).toBe(false)
|
|
})
|
|
|
|
it('back returns previous path after two pushes', () => {
|
|
const { result } = renderNavigationHistory(['/a', '/b'])
|
|
expect(result.current.canGoBack).toBe(true)
|
|
|
|
expect(navigate(result, 'goBack')).toBe('/a')
|
|
expect(result.current.canGoBack).toBe(false)
|
|
expect(result.current.canGoForward).toBe(true)
|
|
})
|
|
|
|
it('forward returns next path after going back', () => {
|
|
const { result } = renderNavigationHistory(['/a', '/b'])
|
|
navigate(result, 'goBack')
|
|
|
|
expect(navigate(result, 'goForward')).toBe('/b')
|
|
expect(result.current.canGoForward).toBe(false)
|
|
})
|
|
|
|
it('push after back clears forward stack', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => { result.current.push('/a'); result.current.push('/b'); result.current.push('/c') })
|
|
act(() => { result.current.goBack() })
|
|
expect(result.current.canGoForward).toBe(true)
|
|
|
|
act(() => { result.current.push('/d') })
|
|
expect(result.current.canGoForward).toBe(false)
|
|
|
|
expect(navigate(result, 'goBack')).toBe('/b')
|
|
})
|
|
|
|
it('duplicate push is a no-op', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => { result.current.push('/a'); result.current.push('/a') })
|
|
expect(result.current.canGoBack).toBe(false)
|
|
})
|
|
|
|
it('goBack skips invalid paths', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => { result.current.push('/a'); result.current.push('/b'); result.current.push('/c') })
|
|
|
|
expect(navigate(result, 'goBack', (p) => p !== '/b')).toBe('/a')
|
|
})
|
|
|
|
it('goForward skips invalid paths', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => { result.current.push('/a'); result.current.push('/b'); result.current.push('/c') })
|
|
act(() => { result.current.goBack(); result.current.goBack() })
|
|
|
|
expect(navigate(result, 'goForward', (p) => p !== '/b')).toBe('/c')
|
|
})
|
|
|
|
it('goBack returns null when nothing valid remains', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => { result.current.push('/a') })
|
|
|
|
expect(navigate(result, 'goBack')).toBeNull()
|
|
})
|
|
|
|
it('goForward returns null at end of history', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => { result.current.push('/a') })
|
|
|
|
expect(navigate(result, 'goForward')).toBeNull()
|
|
})
|
|
|
|
it('removePath adjusts cursor correctly', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => { result.current.push('/a'); result.current.push('/b'); result.current.push('/c') })
|
|
act(() => { result.current.removePath('/b') })
|
|
|
|
expect(navigate(result, 'goBack')).toBe('/a')
|
|
})
|
|
|
|
it('removePath when current note is removed', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => { result.current.push('/a'); result.current.push('/b') })
|
|
act(() => { result.current.removePath('/b') })
|
|
// After removing /b (cursor was at index 1), cursor should adjust
|
|
expect(result.current.canGoBack).toBe(false)
|
|
expect(result.current.canGoForward).toBe(false)
|
|
})
|
|
|
|
it('goBack without predicate returns closed-tab paths (replace scenario)', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
// Simulate: open A, then B replaces A, then C replaces B
|
|
act(() => { result.current.push('/a'); result.current.push('/b'); result.current.push('/c') })
|
|
|
|
// Without a predicate, goBack returns /b even though its tab was replaced
|
|
let target = navigate(result, 'goBack')
|
|
expect(target).toBe('/b')
|
|
|
|
target = navigate(result, 'goBack')
|
|
expect(target).toBe('/a')
|
|
})
|
|
|
|
it('goBack with entry-exists predicate skips deleted notes but returns replaced tabs', () => {
|
|
const { result } = renderNavigationHistory(['/a', '/deleted', '/c'])
|
|
|
|
// Simulate: /deleted was removed from vault, but /a still exists
|
|
const vaultPaths = new Set(['/a', '/c'])
|
|
const isEntryExists = (p) => vaultPaths.has(p)
|
|
|
|
// Should skip /deleted and return /a
|
|
expect(navigate(result, 'goBack', isEntryExists)).toBe('/a')
|
|
})
|
|
|
|
it('goForward with entry-exists predicate skips deleted notes', () => {
|
|
const { result } = renderNavigationHistory(['/a', '/deleted', '/c'])
|
|
navigate(result, 'goBack')
|
|
navigate(result, 'goBack')
|
|
|
|
const vaultPaths = new Set(['/a', '/c'])
|
|
const isEntryExists = (p) => vaultPaths.has(p)
|
|
|
|
// Should skip /deleted and return /c
|
|
expect(navigate(result, 'goForward', isEntryExists)).toBe('/c')
|
|
})
|
|
|
|
it('handles long navigation chain', () => {
|
|
const { result } = renderHook(() => useNavigationHistory())
|
|
act(() => {
|
|
for (let i = 0; i < 10; i++) result.current.push(`/${i}`)
|
|
})
|
|
expect(result.current.canGoBack).toBe(true)
|
|
|
|
// Go all the way back
|
|
for (let i = 8; i >= 0; i--) {
|
|
expect(navigate(result, 'goBack')).toBe(`/${i}`)
|
|
}
|
|
expect(result.current.canGoBack).toBe(false)
|
|
|
|
// Go all the way forward
|
|
for (let i = 1; i <= 9; i++) {
|
|
expect(navigate(result, 'goForward')).toBe(`/${i}`)
|
|
}
|
|
expect(result.current.canGoForward).toBe(false)
|
|
})
|
|
})
|