From 8287509c7c69e88da2b8f80caa60f6945be5ccdd Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Thu, 14 May 2026 22:06:32 +0200 Subject: [PATCH] Sprint: ll (#207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 --------- Co-authored-by: Claude Sonnet 4.6 --- __tests__/components/shared/nav-bar.test.tsx | 41 ++++++++++++++ __tests__/lib/product-switch-path.test.ts | 56 ++++++++++++++++++++ components/shared/nav-bar.tsx | 4 +- lib/product-switch-path.ts | 14 +++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 __tests__/lib/product-switch-path.test.ts create mode 100644 lib/product-switch-path.ts 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}` +}