// @vitest-environment jsdom import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' import React from 'react' const pushMock = vi.fn() const refreshMock = vi.fn() const pathnameMock = vi.fn(() => '/dashboard') vi.mock('next/navigation', () => ({ useRouter: () => ({ push: pushMock, refresh: refreshMock }), usePathname: () => pathnameMock(), })) vi.mock('@/actions/active-product', () => ({ setActiveProductAction: vi.fn(), })) vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() }, })) vi.mock('@/components/ui/dropdown-menu', () => { type Props = React.HTMLAttributes & { children?: React.ReactNode onClick?: () => void } const PassThrough = ({ children }: Props) => <>{children} const Forwarding = ({ children, ...rest }: Props) =>
{children}
return { DropdownMenu: PassThrough, DropdownMenuTrigger: Forwarding, DropdownMenuContent: PassThrough, DropdownMenuItem: ({ children, onClick, className }: Props) => ( ), DropdownMenuSeparator: () => null, } }) vi.mock('@/components/ui/tooltip', () => { type Props = { children?: React.ReactNode } const PassThrough = ({ children }: Props) => <>{children} return { Tooltip: PassThrough, TooltipContent: PassThrough, TooltipProvider: PassThrough, TooltipTrigger: PassThrough, } }) vi.mock('@/components/shared/app-icon', () => ({ AppIcon: () => null })) vi.mock('@/components/shared/user-menu', () => ({ UserMenu: () => null })) vi.mock('@/components/shared/notifications-bell', () => ({ NotificationsBell: () => null })) vi.mock('@/components/solo/nav-status-indicators', () => ({ SoloNavStatusIndicators: () => null, })) import { setActiveProductAction } from '@/actions/active-product' import { toast } from 'sonner' import { NavBar } from '@/components/shared/nav-bar' const actionMock = setActiveProductAction as unknown as ReturnType const toastSuccess = toast.success as unknown as ReturnType const products = [ { id: 'A', name: 'Alpha' }, { id: 'B', name: 'Beta' }, ] function renderNavBar(overrides: { isDemo?: boolean; activeProductId?: string } = {}) { const isDemo = overrides.isDemo ?? false const activeId = overrides.activeProductId ?? 'A' const activeProduct = products.find(p => p.id === activeId) ?? null return render( , ) } beforeEach(() => { vi.clearAllMocks() actionMock.mockResolvedValue({ success: true }) pathnameMock.mockReturnValue('/dashboard') }) describe('NavBar — product switch', () => { it('demo: clicking another product navigates via router.push without calling the action', () => { renderNavBar({ isDemo: true, activeProductId: 'A' }) fireEvent.click(screen.getByText('Beta')) expect(pushMock).toHaveBeenCalledWith('/products/B') expect(actionMock).not.toHaveBeenCalled() expect(toastSuccess).not.toHaveBeenCalled() }) it('non-demo: clicking another product calls setActiveProductAction', async () => { renderNavBar({ isDemo: false, activeProductId: 'A' }) fireEvent.click(screen.getByText('Beta')) await Promise.resolve() expect(actionMock).toHaveBeenCalledWith('B') }) it('non-demo: on /products/A navigates to /products/B', async () => { pathnameMock.mockReturnValue('/products/A') renderNavBar({ isDemo: false, activeProductId: 'A' }) fireEvent.click(screen.getByText('Beta')) await Promise.resolve() await Promise.resolve() expect(pushMock).toHaveBeenCalledWith('/products/B') expect(toastSuccess).toHaveBeenCalled() }) it('non-demo: on /products/A/sprint/SPR1 navigates to /products/B/sprint', async () => { pathnameMock.mockReturnValue('/products/A/sprint/SPR1') renderNavBar({ isDemo: false, activeProductId: 'A' }) fireEvent.click(screen.getByText('Beta')) await Promise.resolve() await Promise.resolve() expect(pushMock).toHaveBeenCalledWith('/products/B/sprint') expect(toastSuccess).toHaveBeenCalled() }) it('non-demo: on /products/A/solo navigates to /products/B/solo', async () => { pathnameMock.mockReturnValue('/products/A/solo') renderNavBar({ isDemo: false, activeProductId: 'A' }) fireEvent.click(screen.getByText('Beta')) await Promise.resolve() await Promise.resolve() expect(pushMock).toHaveBeenCalledWith('/products/B/solo') expect(toastSuccess).toHaveBeenCalled() }) it('non-demo: on /dashboard calls router.refresh and not router.push', async () => { pathnameMock.mockReturnValue('/dashboard') renderNavBar({ isDemo: false, activeProductId: 'A' }) fireEvent.click(screen.getByText('Beta')) await Promise.resolve() await Promise.resolve() expect(refreshMock).toHaveBeenCalled() expect(pushMock).not.toHaveBeenCalled() expect(toastSuccess).toHaveBeenCalled() }) }) describe('NavBar — URL-derived active product (demo only)', () => { it('demo: label and dropdown highlight follow pathname, not the activeProduct prop', () => { pathnameMock.mockReturnValue('/products/B/sprint') const { container } = renderNavBar({ isDemo: true, activeProductId: 'A' }) const trigger = container.querySelector('[data-debug-id="nav-bar__product-switcher"]') expect(trigger?.textContent).toContain('Beta') expect(trigger?.textContent).not.toContain('Alpha') const items = screen.getAllByTestId('dd-item') const itemB = items.find(el => el.textContent?.includes('Beta')) expect(itemB?.className).toContain('bg-primary-container') const itemA = items.find(el => el.textContent?.includes('Alpha')) expect(itemA?.className ?? '').not.toContain('bg-primary-container') }) it('non-demo: pathname does NOT override the activeProduct prop', () => { pathnameMock.mockReturnValue('/products/B/sprint') renderNavBar({ isDemo: false, activeProductId: 'A' }) // Label still reflects server-rendered activeProduct (Alpha) const items = screen.getAllByTestId('dd-item') const itemA = items.find(el => el.textContent?.includes('Alpha')) expect(itemA?.className).toContain('bg-primary-container') }) })