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