/**
* Tests for NavigationContext - navigation-context.tsx
*
* Tests navigation state transitions, custom navigation providers,
* and Link component integration.
*/
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import {
NavigationProvider,
useNavigation,
type NavigationContextValue,
} from '../../context/navigation-context'
import type { NavigationProvider as NavigationProviderInterface, LinkProps } from '../../types'
import type { ComponentType } from 'react'
// Helper component to consume navigation context
function NavigationConsumer({ onNavigation }: { onNavigation: (nav: NavigationContextValue) => void }) {
const nav = useNavigation()
onNavigation(nav)
return (
{nav.getCurrentPath()}
Test Link
)
}
function createMockNavigationProvider(
overrides: Partial = {}
): NavigationProviderInterface {
const MockLink: ComponentType = ({ href, children, className }) => (
{children}
)
return {
navigate: vi.fn(),
getCurrentPath: vi.fn().mockReturnValue('/'),
LinkComponent: MockLink,
...overrides,
}
}
describe('NavigationContext', () => {
describe('NavigationProvider', () => {
it('should provide navigation functions to children', () => {
const mockProvider = createMockNavigationProvider({
getCurrentPath: vi.fn().mockReturnValue('/home'),
})
let navValue: NavigationContextValue | undefined
render(
{ navValue = nav }} />
)
expect(navValue).toBeDefined()
expect(navValue!.navigate).toBeDefined()
expect(navValue!.getCurrentPath).toBeDefined()
expect(navValue!.Link).toBeDefined()
expect(screen.getByTestId('current-path').textContent).toBe('/home')
})
it('should call navigate with path and options', () => {
const mockNavigate = vi.fn()
const mockProvider = createMockNavigationProvider({ navigate: mockNavigate })
render(
{}} />
)
// Click navigate button
fireEvent.click(screen.getByTestId('navigate-btn'))
expect(mockNavigate).toHaveBeenCalledWith('/dashboard')
// Click navigate with replace
fireEvent.click(screen.getByTestId('navigate-replace-btn'))
expect(mockNavigate).toHaveBeenCalledWith('/settings', { replace: true })
})
it('should render custom Link component', () => {
const CustomLink: ComponentType = ({ href, children, className }) => (
Custom: {children}
)
const mockProvider = createMockNavigationProvider({ LinkComponent: CustomLink })
render(
{}} />
)
const link = screen.getByTestId('custom-link')
expect(link).toBeDefined()
expect(link.getAttribute('href')).toBe('/test')
expect(link.className).toContain('test-link')
expect(link.textContent).toBe('Custom: Test Link')
})
it('should use default navigation provider when none provided', () => {
// Mock window.location
const originalLocation = window.location
const mockLocation = {
...originalLocation,
pathname: '/default-path',
href: '/default-path',
}
Object.defineProperty(window, 'location', {
value: mockLocation,
writable: true,
})
render(
{}} />
)
expect(screen.getByTestId('current-path').textContent).toBe('/default-path')
// Restore
Object.defineProperty(window, 'location', {
value: originalLocation,
writable: true,
})
})
it('should provide default Link component that renders anchor tags', () => {
render(
{}} />
)
// Default Link renders as tag
const links = screen.getAllByRole('link')
const testLink = links.find(link => link.textContent === 'Test Link')
expect(testLink).toBeDefined()
expect(testLink?.getAttribute('href')).toBe('/test')
})
})
describe('useNavigation', () => {
it('should throw error when used outside NavigationProvider', () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
expect(() => {
render( {}} />)
}).toThrow('useNavigation must be used within a NavigationProvider')
consoleError.mockRestore()
})
})
describe('navigation state transitions', () => {
it('should track path changes through getCurrentPath', () => {
let currentPath = '/initial'
const mockProvider = createMockNavigationProvider({
getCurrentPath: vi.fn(() => currentPath),
navigate: vi.fn((to: string) => {
currentPath = to
}),
})
const { rerender } = render(
{}} />
)
expect(screen.getByTestId('current-path').textContent).toBe('/initial')
// Simulate navigation
fireEvent.click(screen.getByTestId('navigate-btn'))
// Re-render to see updated path
rerender(
{}} />
)
expect(screen.getByTestId('current-path').textContent).toBe('/dashboard')
})
it('should pass state to navigate when provided', () => {
const mockNavigate = vi.fn()
const mockProvider = createMockNavigationProvider({ navigate: mockNavigate })
function StateNavigator() {
const nav = useNavigation()
return (
)
}
render(
)
fireEvent.click(screen.getByTestId('nav-with-state'))
expect(mockNavigate).toHaveBeenCalledWith('/page', { replace: false, state: { from: 'home' } })
})
})
describe('Link component integration', () => {
it('should render Link with all props passed through', () => {
const MockLink: ComponentType = ({ href, children, className }) => (
{children}
)
const mockProvider = createMockNavigationProvider({ LinkComponent: MockLink })
function LinkRenderer() {
const { Link } = useNavigation()
return (
About Us
)
}
render(
)
const link = screen.getByRole('link')
expect(link.getAttribute('href')).toBe('/about')
expect(link.className).toContain('nav-link')
expect(link.textContent).toBe('About Us')
})
it('should support nested children in Link', () => {
const mockProvider = createMockNavigationProvider()
function NestedLinkRenderer() {
const { Link } = useNavigation()
return (
*
Profile
)
}
render(
)
expect(screen.getByTestId('icon')).toBeDefined()
expect(screen.getByTestId('text')).toBeDefined()
})
})
})