import { beforeEach, describe, expect, it, vi } from 'vitest'
import { render, screen, fireEvent } from '../../../tests/helpers/render'
import { NavigationMenu } from './NavigationMenu'
import type { NavigationTarget } from '../Planner/placeNavigation'
// FE-COMP-NAVMENU-001 to FE-COMP-NAVMENU-007
const TARGETS: NavigationTarget[] = [
{ id: 'google', label: 'Google Maps', url: 'https://maps.example/g' },
{ id: 'waze', label: 'Waze', url: 'https://waze.example/w' },
]
/** A trigger at a chosen viewport position, so the flip can be provoked. */
function anchorAt(top: number, height = 30) {
const el = document.createElement('button')
document.body.appendChild(el)
el.getBoundingClientRect = () => ({
top, bottom: top + height, left: 40, right: 160, width: 120, height,
x: 40, y: top, toJSON: () => ({}),
}) as DOMRect
return el
}
/** jsdom measures everything as 0, so the menu needs a size to be placed. */
function withMenuHeight(height: number) {
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
top: 0, bottom: height, left: 0, right: 200, width: 200, height,
x: 0, y: 0, toJSON: () => ({}),
} as DOMRect)
}
beforeEach(() => {
vi.restoreAllMocks()
document.body.innerHTML = ''
window.innerHeight = 800
window.innerWidth = 1200
})
describe('NavigationMenu', () => {
it('FE-COMP-NAVMENU-001: lists one entry per target', () => {
render()
expect(screen.getByRole('menuitem', { name: 'Google Maps' })).toBeInTheDocument()
expect(screen.getByRole('menuitem', { name: 'Waze' })).toBeInTheDocument()
})
it('FE-COMP-NAVMENU-002: opening a target closes the menu', () => {
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
const onClose = vi.fn()
render()
fireEvent.click(screen.getByRole('menuitem', { name: 'Waze' }))
expect(open).toHaveBeenCalledWith('https://waze.example/w', '_blank', 'noopener,noreferrer')
expect(onClose).toHaveBeenCalledTimes(1)
})
it('FE-COMP-NAVMENU-003: sits below the trigger when there is room', () => {
withMenuHeight(120)
render()
// Trigger ends at 130, so the menu starts just under it.
expect(screen.getByRole('menu')).toHaveStyle({ top: '136px' })
})
it('FE-COMP-NAVMENU-004: flips above the trigger when the bottom edge is close', () => {
// Trigger ends at 780 with a viewport of 800: a 120px menu would be cut off.
withMenuHeight(120)
render()
expect(screen.getByRole('menu')).toHaveStyle({ top: '624px' })
})
it('FE-COMP-NAVMENU-005: never leaves the viewport, even when neither side fits', () => {
// Taller than the window: clamped to the top margin rather than negative.
withMenuHeight(900)
render()
expect(screen.getByRole('menu')).toHaveStyle({ top: '8px' })
})
it('FE-COMP-NAVMENU-006: Escape closes it', () => {
const onClose = vi.fn()
render()
fireEvent.keyDown(document, { key: 'Escape' })
expect(onClose).toHaveBeenCalledTimes(1)
})
it('FE-COMP-NAVMENU-007: renders nothing without targets', () => {
render()
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
})
})