Voeg 4 tests toe die verifiëren dat NavBar na product-wissel naar de juiste URL navigeert: /products/B, /products/B/sprint, /products/B/solo, en router.refresh() op niet-product-pagina's. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
179 lines
6.4 KiB
TypeScript
179 lines
6.4 KiB
TypeScript
// @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<HTMLDivElement> & {
|
|
children?: React.ReactNode
|
|
onClick?: () => void
|
|
}
|
|
const PassThrough = ({ children }: Props) => <>{children}</>
|
|
const Forwarding = ({ children, ...rest }: Props) => <div {...rest}>{children}</div>
|
|
return {
|
|
DropdownMenu: PassThrough,
|
|
DropdownMenuTrigger: Forwarding,
|
|
DropdownMenuContent: PassThrough,
|
|
DropdownMenuItem: ({ children, onClick, className }: Props) => (
|
|
<button type="button" onClick={onClick} className={className} data-testid="dd-item">
|
|
{children}
|
|
</button>
|
|
),
|
|
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<typeof vi.fn>
|
|
const toastSuccess = toast.success as unknown as ReturnType<typeof vi.fn>
|
|
|
|
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(
|
|
<NavBar
|
|
isDemo={isDemo}
|
|
roles={[]}
|
|
userId="u1"
|
|
username="user"
|
|
email={null}
|
|
activeProduct={activeProduct}
|
|
products={products}
|
|
hasActiveSprint={false}
|
|
minQuotaPct={100}
|
|
/>,
|
|
)
|
|
}
|
|
|
|
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')
|
|
})
|
|
})
|