Sprint: ll (#207)

* feat(PBI-ll): voeg lib/product-switch-path.ts toe met resolveProductSwitchTarget

Pure helper die doel-URL bij product-wissel bepaalt; unit-tests dekken alle pad-gevallen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(product-switch-path): dek alle pad-categorieën en null-terugval af

* feat(nav-bar): gebruik resolveProductSwitchTarget bij product-wissel

Vervang router.refresh() door gerichte navigatie via resolveProductSwitchTarget,
zodat product/sprint/solo-pagina's direct naar het nieuwe product navigeren.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(nav-bar): voeg navigatie-assertions toe voor product-wissel

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>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-14 22:06:32 +02:00 committed by GitHub
parent 3ad352c10f
commit 8287509c7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 114 additions and 1 deletions

View file

@ -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)', () => {

View file

@ -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/<old> to /products/<new>', () => {
expect(resolveProductSwitchTarget('/products/old-id', 'new-id')).toBe('/products/new-id')
})
it('maps /products/<old>/ to /products/<new>', () => {
expect(resolveProductSwitchTarget('/products/old-id/', 'new-id')).toBe('/products/new-id')
})
it('maps /products/<old>/sprint to /products/<new>/sprint', () => {
expect(resolveProductSwitchTarget('/products/old-id/sprint', 'new-id')).toBe(
'/products/new-id/sprint',
)
})
it('maps /products/<old>/sprint/<sprintId> to /products/<new>/sprint', () => {
expect(resolveProductSwitchTarget('/products/old-id/sprint/abc123', 'new-id')).toBe(
'/products/new-id/sprint',
)
})
it('maps /products/<old>/sprint/.../planning to /products/<new>/sprint', () => {
expect(resolveProductSwitchTarget('/products/old-id/sprint/abc123/planning', 'new-id')).toBe(
'/products/new-id/sprint',
)
})
it('maps /products/<old>/solo to /products/<new>/solo', () => {
expect(resolveProductSwitchTarget('/products/old-id/solo', 'new-id')).toBe(
'/products/new-id/solo',
)
})
it('falls back to /products/<new> for /products/<old>/settings', () => {
expect(resolveProductSwitchTarget('/products/old-id/settings', 'new-id')).toBe(
'/products/new-id',
)
})
it('falls back to /products/<new> for unknown sub-segments', () => {
expect(resolveProductSwitchTarget('/products/old-id/unknown/deep', 'new-id')).toBe(
'/products/new-id',
)
})
})

View file

@ -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()
})
}

View file

@ -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}`
}