diff --git a/__tests__/components/shared/nav-bar.test.tsx b/__tests__/components/shared/nav-bar.test.tsx index 67fabce..28e9037 100644 --- a/__tests__/components/shared/nav-bar.test.tsx +++ b/__tests__/components/shared/nav-bar.test.tsx @@ -111,6 +111,47 @@ describe('NavBar — product switch', () => { 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)', () => { diff --git a/__tests__/lib/product-switch-path.test.ts b/__tests__/lib/product-switch-path.test.ts new file mode 100644 index 0000000..02983e9 --- /dev/null +++ b/__tests__/lib/product-switch-path.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest' +import { resolveProductSwitchTarget } from '@/lib/product-switch-path' + +describe('resolveProductSwitchTarget', () => { + it('returns null for non-product pages', () => { + expect(resolveProductSwitchTarget('/dashboard', 'new-id')).toBeNull() + expect(resolveProductSwitchTarget('/insights', 'new-id')).toBeNull() + expect(resolveProductSwitchTarget('/ideas', 'new-id')).toBeNull() + expect(resolveProductSwitchTarget('/jobs', 'new-id')).toBeNull() + expect(resolveProductSwitchTarget('/', 'new-id')).toBeNull() + }) + + it('maps /products/ to /products/', () => { + expect(resolveProductSwitchTarget('/products/old-id', 'new-id')).toBe('/products/new-id') + }) + + it('maps /products// to /products/', () => { + expect(resolveProductSwitchTarget('/products/old-id/', 'new-id')).toBe('/products/new-id') + }) + + it('maps /products//sprint to /products//sprint', () => { + expect(resolveProductSwitchTarget('/products/old-id/sprint', 'new-id')).toBe( + '/products/new-id/sprint', + ) + }) + + it('maps /products//sprint/ to /products//sprint', () => { + expect(resolveProductSwitchTarget('/products/old-id/sprint/abc123', 'new-id')).toBe( + '/products/new-id/sprint', + ) + }) + + it('maps /products//sprint/.../planning to /products//sprint', () => { + expect(resolveProductSwitchTarget('/products/old-id/sprint/abc123/planning', 'new-id')).toBe( + '/products/new-id/sprint', + ) + }) + + it('maps /products//solo to /products//solo', () => { + expect(resolveProductSwitchTarget('/products/old-id/solo', 'new-id')).toBe( + '/products/new-id/solo', + ) + }) + + it('falls back to /products/ for /products//settings', () => { + expect(resolveProductSwitchTarget('/products/old-id/settings', 'new-id')).toBe( + '/products/new-id', + ) + }) + + it('falls back to /products/ for unknown sub-segments', () => { + expect(resolveProductSwitchTarget('/products/old-id/unknown/deep', 'new-id')).toBe( + '/products/new-id', + ) + }) +}) diff --git a/components/shared/nav-bar.tsx b/components/shared/nav-bar.tsx index 7b272c1..88e5064 100644 --- a/components/shared/nav-bar.tsx +++ b/components/shared/nav-bar.tsx @@ -20,6 +20,7 @@ import { NotificationsBell } from '@/components/shared/notifications-bell' import { SoloNavStatusIndicators } from '@/components/solo/nav-status-indicators' import { cn } from '@/lib/utils' import { setActiveProductAction } from '@/actions/active-product' +import { resolveProductSwitchTarget } from '@/lib/product-switch-path' import { debugProps } from '@/lib/debug' interface NavBarProps { @@ -70,7 +71,8 @@ export function NavBar({ } const next = products.find(p => p.id === productId) toast.success(`Actief product: ${next?.name ?? 'gewijzigd'}`) - router.refresh() + const target = resolveProductSwitchTarget(pathname, productId) + if (target) router.push(target); else router.refresh() }) } diff --git a/lib/product-switch-path.ts b/lib/product-switch-path.ts new file mode 100644 index 0000000..ce79ab9 --- /dev/null +++ b/lib/product-switch-path.ts @@ -0,0 +1,14 @@ +export function resolveProductSwitchTarget( + pathname: string, + newProductId: string, +): string | null { + const match = pathname.match(/^\/products\/([^/]+)(\/.*)?$/) + if (!match) return null + + const rest = match[2] ?? '' + + if (!rest || rest === '/') return `/products/${newProductId}` + if (rest.startsWith('/sprint')) return `/products/${newProductId}/sprint` + if (rest.startsWith('/solo')) return `/products/${newProductId}/solo` + return `/products/${newProductId}` +}