import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import NavbarClient from '@/components/layout/NavbarClient'
import type { Category } from '@/lib/types'
const mockPush = vi.fn()
vi.mock('next/navigation', () => ({
usePathname: vi.fn(() => '/'),
useRouter: vi.fn(() => ({ push: mockPush })),
}))
const categories: Category[] = [
{ id: '1', name: 'Anime', slug: 'anime', description: 'Anime news' },
{ id: '2', name: 'Games', slug: 'games', description: null },
{ id: '3', name: 'Music', slug: 'music', description: 'Music coverage' },
]
describe('NavbarClient', () => {
beforeEach(() => {
mockPush.mockReset()
document.body.style.overflow = ''
})
it('renders all category links', () => {
render()
expect(screen.getByText('Anime')).toBeInTheDocument()
expect(screen.getByText('Games')).toBeInTheDocument()
expect(screen.getByText('Music')).toBeInTheDocument()
})
it('renders the site logo', () => {
render()
expect(screen.getByText('言羽')).toBeInTheDocument()
})
it('renders a search button', () => {
render()
expect(screen.getByLabelText('Open search')).toBeInTheDocument()
})
it('dispatches custom event when search button is clicked', () => {
const dispatchSpy = vi.fn()
window.dispatchEvent = dispatchSpy
render()
fireEvent.click(screen.getByLabelText('Open search'))
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({ type: 'kotobane:open-search' })
)
})
it('shows mobile hamburger menu button', () => {
render()
expect(screen.getByLabelText('Open menu')).toBeInTheDocument()
})
it('opens mobile overlay when hamburger is clicked', () => {
render()
fireEvent.click(screen.getByLabelText('Open menu'))
expect(screen.getByLabelText('Close menu')).toBeInTheDocument()
})
it('closes mobile overlay when close button is clicked', () => {
render()
fireEvent.click(screen.getByLabelText('Open menu'))
expect(screen.getByLabelText('Close menu')).toBeInTheDocument()
fireEvent.click(screen.getByLabelText('Close menu'))
expect(screen.queryByLabelText('Close menu')).not.toBeInTheDocument()
})
it('closes mobile overlay when backdrop is clicked', () => {
render()
fireEvent.click(screen.getByLabelText('Open menu'))
const backdrop = document.querySelector('.bg-black\\/60')
expect(backdrop).toBeInTheDocument()
if (backdrop) fireEvent.click(backdrop)
expect(screen.queryByLabelText('Close menu')).not.toBeInTheDocument()
})
it('locks body scroll when menu is open, unlocks when closed', () => {
render()
fireEvent.click(screen.getByLabelText('Open menu'))
expect(document.body.style.overflow).toBe('hidden')
fireEvent.click(screen.getByLabelText('Close menu'))
expect(document.body.style.overflow).toBe('')
})
it('marks the current path category as active', async () => {
const { usePathname } = await import('next/navigation')
vi.mocked(usePathname).mockReturnValue('/anime')
render()
const animeLink = screen.getByText('Anime').closest('a')
expect(animeLink).toHaveClass('text-accent')
})
})