| | import type { Params } from '../../server/request/params' |
| | import { parseAppRoute } from '../../shared/lib/router/routes/app' |
| | import type { FallbackRouteParam } from './types' |
| | import { resolveRouteParamsFromTree } from './utils' |
| |
|
| | |
| | type TestLoaderTree = [ |
| | segment: string, |
| | parallelRoutes: { [key: string]: TestLoaderTree }, |
| | modules: Record<string, unknown>, |
| | ] |
| |
|
| | function createLoaderTree( |
| | segment: string, |
| | parallelRoutes: { [key: string]: TestLoaderTree } = {}, |
| | children?: TestLoaderTree |
| | ): TestLoaderTree { |
| | const routes = children ? { ...parallelRoutes, children } : parallelRoutes |
| | return [segment, routes, {}] |
| | } |
| |
|
| | describe('resolveRouteParamsFromTree', () => { |
| | describe('direct match case', () => { |
| | it('should skip processing when param already exists in params object', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[existingParam]'), |
| | }) |
| | const params: Params = { existingParam: 'value' } |
| | const route = parseAppRoute('/some/path', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.existingParam).toBe('value') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should skip processing for multiple existing params', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[param1]'), |
| | modal: createLoaderTree('[...param2]'), |
| | }) |
| | const params: Params = { param1: 'value1', param2: ['a', 'b'] } |
| | const route = parseAppRoute('/some/path', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.param1).toBe('value1') |
| | expect(params.param2).toEqual(['a', 'b']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| |
|
| | describe('dynamic params', () => { |
| | it('should extract dynamic param from pathname when not already in params', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[dynamicParam]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/some/path', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.dynamicParam).toBe('some') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle multiple dynamic params in parallel routes at same depth', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree('', { |
| | modal: createLoaderTree('[id]'), |
| | sidebar: createLoaderTree('[category]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/photo/123', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.id).toBe('photo') |
| | expect(params.category).toBe('photo') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should extract dynamic param from pathname at depth 0', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[category]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/tech', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.category).toBe('tech') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should extract dynamic param from pathname at nested depth', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('blog', { |
| | sidebar: createLoaderTree('[category]'), |
| | }) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog/tech', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.category).toBe('tech') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should extract dynamic param even when other unknown params exist at different depths', () => { |
| | |
| | |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[category]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/tech', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [ |
| | { paramName: 'slug', paramType: 'dynamic' }, |
| | ] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.category).toBe('tech') |
| | expect(fallbackRouteParams).toHaveLength(1) |
| | }) |
| |
|
| | it('should mark dynamic param as fallback when depth exceeds pathname length', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'blog', |
| | {}, |
| | createLoaderTree('posts', { |
| | sidebar: createLoaderTree('[category]'), |
| | }) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.category).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(1) |
| | expect(fallbackRouteParams[0]).toEqual({ |
| | paramName: 'category', |
| | paramType: 'dynamic', |
| | }) |
| | }) |
| |
|
| | it('should resolve embedded params when extracting dynamic param value', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[lang]', { |
| | sidebar: createLoaderTree('[category]'), |
| | }) |
| | ) |
| | const params: Params = { lang: 'en' } |
| | const route = parseAppRoute('/en/tech', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.category).toBe('tech') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should extract dynamic param when unknown params exist at LATER depth', () => { |
| | |
| | |
| | |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '[lang]', |
| | { |
| | sidebar: createLoaderTree('[filter]'), |
| | }, |
| | createLoaderTree('products', {}, createLoaderTree('[category]')) |
| | ) |
| | ) |
| | const params: Params = { lang: 'en' } |
| | const route = parseAppRoute('/en/products/[category]', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [ |
| | { paramName: 'category', paramType: 'dynamic' }, |
| | ] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.filter).toBe('products') |
| | expect(fallbackRouteParams).toHaveLength(1) |
| | }) |
| |
|
| | it('should NOT extract dynamic param when placeholder is at SAME depth', () => { |
| | |
| | |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '[lang]', |
| | {}, |
| | createLoaderTree( |
| | 'products', |
| | {}, |
| | createLoaderTree('[category]', { |
| | sidebar: createLoaderTree('[filter]'), |
| | }) |
| | ) |
| | ) |
| | ) |
| | const params: Params = { lang: 'en' } |
| | const route = parseAppRoute('/en/products/[category]', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [ |
| | { paramName: 'category', paramType: 'dynamic' }, |
| | ] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.filter).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(2) |
| | expect(fallbackRouteParams[1]).toEqual({ |
| | paramName: 'filter', |
| | paramType: 'dynamic', |
| | }) |
| | }) |
| | }) |
| |
|
| | describe('catchall deriving from pathname with depth', () => { |
| | it('should use depth to correctly slice pathname segments', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('blog', { |
| | sidebar: createLoaderTree('[...catchallParam]'), |
| | }) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog/2023/posts/my-article', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.catchallParam).toEqual(['2023', 'posts', 'my-article']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle catchall at depth 0 (root level)', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[...catchallParam]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog/2023/posts', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.catchallParam).toEqual(['blog', '2023', 'posts']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle nested depth correctly', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'products', |
| | {}, |
| | createLoaderTree('[category]', { |
| | filters: createLoaderTree('[...filterPath]'), |
| | }) |
| | ) |
| | ) |
| | const params: Params = { category: 'electronics' } |
| | const route = parseAppRoute('/products/electronics/phones/iphone', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.filterPath).toEqual(['phones', 'iphone']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle single path segment', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[...catchallParam]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/single', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.catchallParam).toEqual(['single']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| |
|
| | describe('route groups', () => { |
| | it('should not increment depth for route groups', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('(marketing)', { |
| | sidebar: createLoaderTree('[...catchallParam]'), |
| | }) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog/post', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.catchallParam).toEqual(['blog', 'post']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle multiple route groups', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '(group1)', |
| | {}, |
| | createLoaderTree( |
| | '(group2)', |
| | {}, |
| | createLoaderTree('blog', { |
| | sidebar: createLoaderTree('[...path]'), |
| | }) |
| | ) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog/2023/posts', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.path).toEqual(['2023', 'posts']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| |
|
| | describe('optional-catchall with empty pathname', () => { |
| | it('should set params to empty array when pathname has no segments', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[[...optionalCatchall]]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.optionalCatchall).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle optional catchall at nested depth with no remaining segments', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('blog', { |
| | sidebar: createLoaderTree('[[...optionalPath]]'), |
| | }) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.optionalPath).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| |
|
| | describe('optional-catchall with non-empty pathname', () => { |
| | it('should populate params with path segments', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[[...optionalCatchall]]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/api/v1/users', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.optionalCatchall).toEqual(['api', 'v1', 'users']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| |
|
| | describe('catchall-intercepted params', () => { |
| | it('should handle catchall-intercepted params in parallel routes', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree('', { |
| | modal: createLoaderTree('[...path]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/photos/album/2023', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.path).toEqual(['photos', 'album', '2023']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| |
|
| | describe('error cases', () => { |
| | it('should throw error for catchall with empty pathname', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[...catchallParam]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | expect(() => |
| | resolveRouteParamsFromTree( |
| | loaderTree, |
| | params, |
| | route, |
| | fallbackRouteParams |
| | ) |
| | ).toThrow(/Unexpected empty path segments/) |
| | }) |
| |
|
| | it('should throw error for catchall when depth exceeds pathname', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'blog', |
| | {}, |
| | createLoaderTree('posts', { |
| | sidebar: createLoaderTree('[...catchallParam]'), |
| | }) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | expect(() => |
| | resolveRouteParamsFromTree( |
| | loaderTree, |
| | params, |
| | route, |
| | fallbackRouteParams |
| | ) |
| | ).toThrow(/Unexpected empty path segments/) |
| | }) |
| | }) |
| |
|
| | describe('complex scenarios', () => { |
| | it('should handle multiple parallel routes at same level', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[...sidebarPath]'), |
| | modal: createLoaderTree('[[...modalPath]]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/products/electronics', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.sidebarPath).toEqual(['products', 'electronics']) |
| | expect(params.modalPath).toEqual(['products', 'electronics']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle parallel route with embedded dynamic param from pathname', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[lang]', { |
| | sidebar: createLoaderTree('[...path]'), |
| | }) |
| | ) |
| | const params: Params = { lang: 'en' } |
| | const route = parseAppRoute('/en/blog/post', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.path).toEqual(['blog', 'post']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should only process parallel routes, not children route', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | { |
| | sidebar: createLoaderTree('[...path]'), |
| | }, |
| | createLoaderTree('blog') |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog/post', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.path).toEqual(['blog', 'post']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| |
|
| | describe('interception routes', () => { |
| | it('should increment depth for (.) interception route (same level)', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('(.)photo', { |
| | modal: createLoaderTree('[...segments]'), |
| | }) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/photo/123/details', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.segments).toEqual(['123', 'details']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should increment depth for (..) interception route (parent level)', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'gallery', |
| | {}, |
| | createLoaderTree('(..)photo', { |
| | modal: createLoaderTree('[id]'), |
| | }) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/gallery/photo/123', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.id).toBe('123') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should increment depth for (...) interception route (root level)', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'app', |
| | {}, |
| | createLoaderTree( |
| | 'gallery', |
| | {}, |
| | createLoaderTree('(...)photo', { |
| | modal: createLoaderTree('[...path]'), |
| | }) |
| | ) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/app/gallery/photo/2023/album', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.path).toEqual(['2023', 'album']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should increment depth for (..)(..) interception route (grandparent level)', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'a', |
| | {}, |
| | createLoaderTree( |
| | 'b', |
| | {}, |
| | createLoaderTree('(..)(..)photo', { |
| | modal: createLoaderTree('[category]'), |
| | }) |
| | ) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/a/b/photo/nature', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.category).toBe('nature') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should distinguish interception routes from regular route groups', () => { |
| | |
| | |
| | const routeGroupTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('(marketing)', { |
| | sidebar: createLoaderTree('[...path]'), |
| | }) |
| | ) |
| |
|
| | const interceptionTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('(.)photo', { |
| | modal: createLoaderTree('[...path]'), |
| | }) |
| | ) |
| |
|
| | const route = parseAppRoute('/photo/123', true) |
| |
|
| | |
| | const routeGroupParams: Params = {} |
| | const routeGroupFallback: FallbackRouteParam[] = [] |
| | resolveRouteParamsFromTree( |
| | routeGroupTree, |
| | routeGroupParams, |
| | route, |
| | routeGroupFallback |
| | ) |
| | |
| | expect(routeGroupParams.path).toEqual(['photo', '123']) |
| |
|
| | |
| | const interceptionParams: Params = {} |
| | const interceptionFallback: FallbackRouteParam[] = [] |
| | resolveRouteParamsFromTree( |
| | interceptionTree, |
| | interceptionParams, |
| | route, |
| | interceptionFallback |
| | ) |
| | |
| | expect(interceptionParams.path).toEqual(['123']) |
| | }) |
| | }) |
| |
|
| | describe('empty pathname edge cases', () => { |
| | it('should mark dynamic param as fallback when pathname is empty', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | modal: createLoaderTree('[id]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.id).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(1) |
| | expect(fallbackRouteParams[0]).toEqual({ |
| | paramName: 'id', |
| | paramType: 'dynamic', |
| | }) |
| | }) |
| |
|
| | it('should mark multiple dynamic params as fallback when pathname is empty', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | modal: createLoaderTree('[category]'), |
| | sidebar: createLoaderTree('[filter]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | expect(params.category).toBeUndefined() |
| | expect(params.filter).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(2) |
| | expect(fallbackRouteParams).toContainEqual({ |
| | paramName: 'category', |
| | paramType: 'dynamic', |
| | }) |
| | expect(fallbackRouteParams).toContainEqual({ |
| | paramName: 'filter', |
| | paramType: 'dynamic', |
| | }) |
| | }) |
| |
|
| | it('should handle nested parallel route with empty pathname at that depth', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('blog', { |
| | modal: createLoaderTree('[id]'), |
| | }) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.id).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(1) |
| | expect(fallbackRouteParams[0]).toEqual({ |
| | paramName: 'id', |
| | paramType: 'dynamic', |
| | }) |
| | }) |
| | }) |
| |
|
| | describe('complex path segments', () => { |
| | it('should handle catch-all with embedded param placeholders in pathname', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[...path]'), |
| | }) |
| | const params: Params = {} |
| | const route = parseAppRoute('/blog/[category]/tech', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [ |
| | { paramName: 'category', paramType: 'dynamic' }, |
| | ] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.path).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(2) |
| | expect(fallbackRouteParams[1]).toEqual({ |
| | paramName: 'path', |
| | paramType: 'catchall', |
| | }) |
| | }) |
| |
|
| | it('should mark catch-all as fallback when pathname has unknown param placeholder', () => { |
| | |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[lang]', { |
| | sidebar: createLoaderTree('[...path]'), |
| | }) |
| | ) |
| | const params: Params = { lang: 'en' } |
| | const route = parseAppRoute('/en/blog/[category]', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.path).toBeUndefined() |
| | expect(fallbackRouteParams).toHaveLength(1) |
| | expect(fallbackRouteParams[0]).toEqual({ |
| | paramName: 'path', |
| | paramType: 'catchall', |
| | }) |
| | }) |
| |
|
| | it('should handle mixed static and dynamic segments in catch-all resolution', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'products', |
| | {}, |
| | createLoaderTree('[category]', { |
| | filters: createLoaderTree('[...filterPath]'), |
| | }) |
| | ) |
| | ) |
| | const params: Params = { category: 'electronics' } |
| | const route = parseAppRoute( |
| | '/products/electronics/brand/apple/price/high', |
| | true |
| | ) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.filterPath).toEqual(['brand', 'apple', 'price', 'high']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| |
|
| | describe('integration scenarios', () => { |
| | it('should handle interception route + parallel route together', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'gallery', |
| | {}, |
| | createLoaderTree('(.)photo', { |
| | modal: createLoaderTree('[id]'), |
| | sidebar: createLoaderTree('[category]'), |
| | }) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/gallery/photo/123', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.id).toBe('123') |
| | expect(params.category).toBe('123') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle route group + parallel route + interception route', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '(marketing)', |
| | {}, |
| | createLoaderTree( |
| | 'gallery', |
| | {}, |
| | createLoaderTree('(.)photo', { |
| | modal: createLoaderTree('[...path]'), |
| | }) |
| | ) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/gallery/photo/2023/album', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | |
| | expect(params.path).toEqual(['2023', 'album']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle all param types together', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[lang]', { |
| | modal: createLoaderTree('[category]'), |
| | sidebar: createLoaderTree('[...tags]'), |
| | info: createLoaderTree('[[...extra]]'), |
| | }) |
| | ) |
| | const params: Params = { lang: 'en' } |
| | const route = parseAppRoute('/en/tech/react/nextjs', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.category).toBe('tech') |
| | expect(params.tags).toEqual(['tech', 'react', 'nextjs']) |
| | expect(params.extra).toEqual(['tech', 'react', 'nextjs']) |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| |
|
| | it('should handle complex nesting with multiple interception routes', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'app', |
| | {}, |
| | createLoaderTree( |
| | '(.)modal', |
| | {}, |
| | createLoaderTree('(.)photo', { |
| | dialog: createLoaderTree('[id]'), |
| | }) |
| | ) |
| | ) |
| | ) |
| | const params: Params = {} |
| | const route = parseAppRoute('/app/modal/photo/image-123', true) |
| | const fallbackRouteParams: FallbackRouteParam[] = [] |
| |
|
| | resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) |
| |
|
| | |
| | expect(params.id).toBe('image-123') |
| | expect(fallbackRouteParams).toHaveLength(0) |
| | }) |
| | }) |
| | }) |
| |
|