| | import { |
| | createOpaqueFallbackRouteParams, |
| | getFallbackRouteParams, |
| | } from './fallback-params' |
| | import type { FallbackRouteParam } from '../../build/static-paths/types' |
| | import type AppPageRouteModule from '../route-modules/app-page/module' |
| | import type { LoaderTree } from '../lib/app-dir-module' |
| |
|
| | |
| | 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, {}] |
| | } |
| |
|
| | |
| | |
| | |
| | function createMockRouteModule(loaderTree: LoaderTree): AppPageRouteModule { |
| | return { |
| | userland: { |
| | loaderTree, |
| | }, |
| | } as AppPageRouteModule |
| | } |
| |
|
| | describe('createOpaqueFallbackRouteParams', () => { |
| | describe('opaque object interface', () => { |
| | const fallbackParams: readonly FallbackRouteParam[] = [ |
| | { paramName: 'slug', paramType: 'dynamic' }, |
| | { paramName: 'modal', paramType: 'dynamic' }, |
| | ] |
| |
|
| | it('has method works correctly', () => { |
| | const result = createOpaqueFallbackRouteParams(fallbackParams)! |
| |
|
| | expect(result.has('slug')).toBe(true) |
| | expect(result.has('modal')).toBe(true) |
| | expect(result.has('nonexistent')).toBe(false) |
| | expect(result.has('')).toBe(false) |
| | }) |
| |
|
| | it('get method works correctly', () => { |
| | const result = createOpaqueFallbackRouteParams(fallbackParams)! |
| |
|
| | expect(result.get('slug')?.[0]).toMatch(/^%%drp:slug:[a-f0-9]+%%$/) |
| | expect(result.get('modal')?.[0]).toMatch(/^%%drp:modal:[a-f0-9]+%%$/) |
| | expect(result.get('nonexistent')).toBeUndefined() |
| | expect(result.get('')).toBeUndefined() |
| | }) |
| |
|
| | it('iterator yields correct entries', () => { |
| | const result = createOpaqueFallbackRouteParams(fallbackParams)! |
| |
|
| | const entries = Array.from(result.entries()) |
| | expect(entries).toHaveLength(2) |
| |
|
| | const [name, [value]] = entries[0] |
| | expect(name).toBe('slug') |
| | expect(value).toMatch(/^%%drp:slug:[a-f0-9]+%%$/) |
| | }) |
| | }) |
| | }) |
| |
|
| | describe('getFallbackRouteParams', () => { |
| | describe('Regular Routes (children segments)', () => { |
| | it('should extract single dynamic segment from children route', () => { |
| | |
| | const loaderTree = createLoaderTree('', {}, createLoaderTree('[slug]')) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[slug]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.has('slug')).toBe(true) |
| | expect(result!.get('slug')?.[1]).toBe('d') |
| | }) |
| |
|
| | it('should extract multiple nested dynamic segments', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[category]', {}, createLoaderTree('[slug]')) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[category]/[slug]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(2) |
| | expect(result!.has('category')).toBe(true) |
| | expect(result!.has('slug')).toBe(true) |
| | expect(result!.get('category')?.[1]).toBe('d') |
| | expect(result!.get('slug')?.[1]).toBe('d') |
| | }) |
| |
|
| | it('should extract catchall segment', () => { |
| | |
| | const loaderTree = createLoaderTree('', {}, createLoaderTree('[...slug]')) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[...slug]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('slug')).toBe(true) |
| | expect(result!.get('slug')?.[1]).toBe('c') |
| | }) |
| |
|
| | it('should extract optional catchall segment', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[[...slug]]') |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[[...slug]]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('slug')).toBe(true) |
| | expect(result!.get('slug')?.[1]).toBe('oc') |
| | }) |
| |
|
| | it('should extract mixed static and dynamic segments', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'blog', |
| | {}, |
| | createLoaderTree( |
| | '[category]', |
| | {}, |
| | createLoaderTree('posts', {}, createLoaderTree('[slug]')) |
| | ) |
| | ) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/blog/[category]/posts/[slug]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(2) |
| | expect(result!.has('category')).toBe(true) |
| | expect(result!.has('slug')).toBe(true) |
| | }) |
| |
|
| | it('should handle route with no dynamic segments', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('blog', {}, createLoaderTree('posts')) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/blog/posts', routeModule) |
| |
|
| | |
| | expect(result).toBeNull() |
| | }) |
| |
|
| | it('should handle partially static routes', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[teamSlug]', {}, createLoaderTree('[projectSlug]')) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/vercel/[projectSlug]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | |
| | expect(result!.has('projectSlug')).toBe(true) |
| | expect(result!.has('teamSlug')).toBe(false) |
| | }) |
| | }) |
| |
|
| | describe('Route Groups', () => { |
| | it('should ignore route groups when extracting segments', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '(marketing)', |
| | {}, |
| | createLoaderTree('blog', {}, createLoaderTree('[slug]')) |
| | ) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/blog/[slug]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('slug')).toBe(true) |
| | }) |
| |
|
| | it('should handle route groups mixed with static segments', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '(app)', |
| | {}, |
| | createLoaderTree( |
| | 'dashboard', |
| | {}, |
| | createLoaderTree('(users)', {}, createLoaderTree('[userId]')) |
| | ) |
| | ) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/dashboard/[userId]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('userId')).toBe(true) |
| | }) |
| | }) |
| |
|
| | describe('Parallel Routes', () => { |
| | it('should extract segment from parallel route matching pathname', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | modal: createLoaderTree('[id]'), |
| | }) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[id]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('id')).toBe(true) |
| | }) |
| |
|
| | it('should extract segments from both children and parallel routes', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[lang]', { |
| | modal: createLoaderTree('[photoId]'), |
| | }) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[lang]/[photoId]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(2) |
| | expect(result!.has('lang')).toBe(true) |
| | expect(result!.has('photoId')).toBe(true) |
| | }) |
| |
|
| | it('should handle parallel route params that are not in pathname', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[id]', { |
| | modal: createLoaderTree('[photoId]'), |
| | }) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[id]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(2) |
| | expect(result!.has('id')).toBe(true) |
| | |
| | expect(result!.has('photoId')).toBe(true) |
| | }) |
| | }) |
| |
|
| | describe('Interception Routes', () => { |
| | it('should extract segment from (.) same-level interception route', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('(.)photo', {}, createLoaderTree('[photoId]')) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/(.)photo/[photoId]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('photoId')).toBe(true) |
| | }) |
| |
|
| | it('should extract segment from (..) parent-level interception route', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'gallery', |
| | {}, |
| | createLoaderTree('(..)photo', {}, createLoaderTree('[photoId]')) |
| | ) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/gallery/(..)photo/[photoId]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('photoId')).toBe(true) |
| | }) |
| |
|
| | it('should extract intercepted param when marker is part of the segment itself', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('(.)[photoId]') |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[photoId]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('photoId')).toBe(true) |
| | |
| | expect(result!.get('photoId')?.[1]).toBe('di(.)') |
| | }) |
| | }) |
| |
|
| | describe('Interception Routes in Parallel Routes', () => { |
| | it('should extract segment from interception route in parallel slot', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | modal: createLoaderTree('(.)photo', {}, createLoaderTree('[photoId]')), |
| | }) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/(.)photo/[photoId]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('photoId')).toBe(true) |
| | }) |
| |
|
| | it('should extract segments from both children and intercepting parallel route', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[id]', { |
| | modal: createLoaderTree( |
| | '(.)photo', |
| | {}, |
| | createLoaderTree('[photoId]') |
| | ), |
| | }) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/[id]/(.)photo/[photoId]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(2) |
| | expect(result!.has('id')).toBe(true) |
| | expect(result!.has('photoId')).toBe(true) |
| | }) |
| |
|
| | it('should handle realistic photo gallery pattern with interception', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | 'photos', |
| | {}, |
| | createLoaderTree('[id]', { |
| | modal: createLoaderTree( |
| | '(.)photo', |
| | {}, |
| | createLoaderTree('[photoId]') |
| | ), |
| | }) |
| | ) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/photos/[id]/(.)photo/[photoId]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(2) |
| | expect(result!.has('id')).toBe(true) |
| | expect(result!.has('photoId')).toBe(true) |
| | }) |
| | }) |
| |
|
| | describe('Complex Mixed Scenarios', () => { |
| | it('should handle route groups + parallel routes + interception routes', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '(marketing)', |
| | {}, |
| | createLoaderTree('[lang]', { |
| | modal: createLoaderTree( |
| | '(.)photo', |
| | {}, |
| | createLoaderTree('[photoId]') |
| | ), |
| | }) |
| | ) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/[lang]/(.)photo/[photoId]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(2) |
| | expect(result!.has('lang')).toBe(true) |
| | expect(result!.has('photoId')).toBe(true) |
| | }) |
| |
|
| | it('should handle i18n with interception routes', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '[locale]', |
| | {}, |
| | createLoaderTree( |
| | 'products', |
| | {}, |
| | createLoaderTree('[category]', { |
| | modal: createLoaderTree( |
| | '(.)product', |
| | {}, |
| | createLoaderTree('[productId]') |
| | ), |
| | }) |
| | ) |
| | ) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/[locale]/products/[category]/(.)product/[productId]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(3) |
| | expect(result!.has('locale')).toBe(true) |
| | expect(result!.has('category')).toBe(true) |
| | expect(result!.has('productId')).toBe(true) |
| | }) |
| |
|
| | it('should handle partially static i18n route', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree( |
| | '[locale]', |
| | {}, |
| | createLoaderTree('products', {}, createLoaderTree('[category]')) |
| | ) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/en/products/[category]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('category')).toBe(true) |
| | |
| | expect(result!.has('locale')).toBe(false) |
| | }) |
| |
|
| | it('should handle a partially static intercepting route', () => { |
| | |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('[locale]', { |
| | modal: createLoaderTree( |
| | '(.)photo', |
| | {}, |
| | createLoaderTree('[photoId]') |
| | ), |
| | }) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams( |
| | '/en/(.)photo/[photoId]', |
| | routeModule |
| | ) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('photoId')).toBe(true) |
| | |
| | expect(result!.has('locale')).toBe(false) |
| | }) |
| | }) |
| |
|
| | describe('Edge Cases', () => { |
| | it('should return null for pathname with no dynamic segments', () => { |
| | const loaderTree = createLoaderTree( |
| | '', |
| | {}, |
| | createLoaderTree('blog', {}, createLoaderTree('posts')) |
| | ) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/blog/posts', routeModule) |
| |
|
| | expect(result).toBeNull() |
| | }) |
| |
|
| | it('should handle empty segment in tree', () => { |
| | |
| | const loaderTree = createLoaderTree('', {}, createLoaderTree('[id]')) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[id]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('id')).toBe(true) |
| | }) |
| |
|
| | it('should handle root dynamic route', () => { |
| | |
| | const loaderTree = createLoaderTree('', {}, createLoaderTree('[slug]')) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[slug]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('slug')).toBe(true) |
| | }) |
| |
|
| | it('should handle catchall at root', () => { |
| | |
| | const loaderTree = createLoaderTree('', {}, createLoaderTree('[...slug]')) |
| | const routeModule = createMockRouteModule(loaderTree) |
| | const result = getFallbackRouteParams('/[...slug]', routeModule) |
| |
|
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('slug')).toBe(true) |
| | expect(result!.get('slug')?.[1]).toBe('c') |
| | }) |
| |
|
| | it('should handle optional catchall in parallel route', () => { |
| | |
| | const loaderTree = createLoaderTree('', { |
| | sidebar: createLoaderTree('[[...optional]]'), |
| | }) |
| | const routeModule = createMockRouteModule(loaderTree) |
| |
|
| | let result = getFallbackRouteParams('/[[...optional]]', routeModule) |
| | expect(result).not.toBeNull() |
| | expect(result!.size).toBe(1) |
| | expect(result!.has('optional')).toBe(true) |
| | expect(result!.get('optional')?.[1]).toBe('oc') |
| |
|
| | result = getFallbackRouteParams('/sidebar/is/real', routeModule) |
| | expect(result).toBeNull() |
| | }) |
| | }) |
| | }) |
| |
|