next.js/packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.test.ts
extract-pathname-route-param-segments-from-loader-tree.test.ts1357 lines44.1 KB
import { parseNormalizedAppRoute } from '../../../shared/lib/router/routes/app'
import { extractPathnameRouteParamSegmentsFromLoaderTree } from './extract-pathname-route-param-segments-from-loader-tree'

// Helper to create LoaderTree structures for testing
type TestLoaderTree = [
  segment: string,
  parallelRoutes: { [key: string]: TestLoaderTree },
  modules: Record<string, unknown>,
  staticSiblings: readonly string[] | null,
]

function createLoaderTree(
  segment: string,
  parallelRoutes: { [key: string]: TestLoaderTree } = {},
  children?: TestLoaderTree
): TestLoaderTree {
  const routes = children ? { ...parallelRoutes, children } : parallelRoutes
  return [segment, routes, {}, null]
}

describe('extractPathnameRouteParamSegmentsFromLoaderTree', () => {
  describe('Regular Routes (children segments)', () => {
    it('should extract single dynamic segment from children route', () => {
      // Tree: /[slug]
      const loaderTree = createLoaderTree('', {}, createLoaderTree('[slug]'))
      const route = parseNormalizedAppRoute('/[slug]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[slug]', paramName: 'slug', paramType: 'dynamic' },
      ])
    })

    it('should extract multiple nested dynamic segments', () => {
      // Tree: /[category]/[slug]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[category]', {}, createLoaderTree('[slug]'))
      )
      const route = parseNormalizedAppRoute('/[category]/[slug]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[category]', paramName: 'category', paramType: 'dynamic' },
        { name: '[slug]', paramName: 'slug', paramType: 'dynamic' },
      ])
    })

    it('should extract catchall segment', () => {
      // Tree: /[...slug]
      const loaderTree = createLoaderTree('', {}, createLoaderTree('[...slug]'))
      const route = parseNormalizedAppRoute('/[...slug]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[...slug]', paramName: 'slug', paramType: 'catchall' },
      ])
    })

    it('should extract optional catchall segment', () => {
      // Tree: /[[...slug]]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[[...slug]]')
      )
      const route = parseNormalizedAppRoute('/[[...slug]]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '[[...slug]]',
          paramName: 'slug',
          paramType: 'optional-catchall',
        },
      ])
    })

    it('should extract mixed static and dynamic segments', () => {
      // Tree: /blog/[category]/posts/[slug]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'blog',
          {},
          createLoaderTree(
            '[category]',
            {},
            createLoaderTree('posts', {}, createLoaderTree('[slug]'))
          )
        )
      )
      const route = parseNormalizedAppRoute('/blog/[category]/posts/[slug]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[category]', paramName: 'category', paramType: 'dynamic' },
        { name: '[slug]', paramName: 'slug', paramType: 'dynamic' },
      ])
    })

    it('should handle route with no dynamic segments', () => {
      // Tree: /blog/posts
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {}, createLoaderTree('posts'))
      )
      const route = parseNormalizedAppRoute('/blog/posts')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([])
    })

    it('should extract only segments matching the target pathname', () => {
      // Tree: /blog/[category] but target pathname is /[category]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {}, createLoaderTree('[category]'))
      )
      const route = parseNormalizedAppRoute('/[category]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      // Should not match because depths don't align
      expect(result).toEqual([])
    })
  })

  describe('Route Groups', () => {
    it('should ignore route groups when extracting segments', () => {
      // Tree: /(marketing)/blog/[slug]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '(marketing)',
          {},
          createLoaderTree('blog', {}, createLoaderTree('[slug]'))
        )
      )
      const route = parseNormalizedAppRoute('/blog/[slug]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[slug]', paramName: 'slug', paramType: 'dynamic' },
      ])
    })

    it('should ignore nested route groups', () => {
      // Tree: /(group1)/(group2)/[id]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '(group1)',
          {},
          createLoaderTree('(group2)', {}, createLoaderTree('[id]'))
        )
      )
      const route = parseNormalizedAppRoute('/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
      ])
    })

    it('should handle route groups mixed with static segments', () => {
      // Tree: /(app)/dashboard/(users)/[userId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '(app)',
          {},
          createLoaderTree(
            'dashboard',
            {},
            createLoaderTree('(users)', {}, createLoaderTree('[userId]'))
          )
        )
      )
      const route = parseNormalizedAppRoute('/dashboard/[userId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[userId]', paramName: 'userId', paramType: 'dynamic' },
      ])
    })
  })

  describe('Parallel Routes', () => {
    it('should extract segment from parallel route matching pathname', () => {
      // Tree: / -> @modal/[id]
      const loaderTree = createLoaderTree('', {
        modal: createLoaderTree('[id]'),
      })
      const route = parseNormalizedAppRoute('/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
      ])
    })

    it('should extract segments from multiple parallel routes at same depth', () => {
      // Tree: / -> @modal/[id] + @sidebar/[category]
      const loaderTree = createLoaderTree('', {
        modal: createLoaderTree('[id]'),
        sidebar: createLoaderTree('[category]'),
      })
      const route = parseNormalizedAppRoute('/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      // Only [id] matches - [category] has different param name
      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
      ])
    })

    it('should extract segments from both children and parallel routes', () => {
      // Tree: /[lang] -> children + @modal/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[lang]', {
          modal: createLoaderTree('[photoId]'),
        })
      )
      const route = parseNormalizedAppRoute('/[lang]/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[lang]', paramName: 'lang', paramType: 'dynamic' },
        { name: '[photoId]', paramName: 'photoId', paramType: 'dynamic' },
      ])
    })

    it('should extract catchall from parallel route', () => {
      // Tree: / -> @sidebar/[...path]
      const loaderTree = createLoaderTree('', {
        sidebar: createLoaderTree('[...path]'),
      })
      const route = parseNormalizedAppRoute('/[...path]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[...path]', paramName: 'path', paramType: 'catchall' },
      ])
    })

    it('should NOT extract parallel route segments that do not match pathname', () => {
      // Tree: /[id] -> @modal/[photoId] + @sidebar/[category]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[id]', {
          modal: createLoaderTree('[photoId]'),
          sidebar: createLoaderTree('[category]'),
        })
      )
      const route = parseNormalizedAppRoute('/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      // Only [id] should match, parallel routes are at depth 1
      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
      ])
    })
  })

  describe('Interception Routes', () => {
    it('should extract segment from (.) same-level interception route', () => {
      // Tree: /(.)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('(.)photo', {}, createLoaderTree('[photoId]'))
      )
      const route = parseNormalizedAppRoute('/(.)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should extract segment from (..) parent-level interception route', () => {
      // Tree: /gallery/(..)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'gallery',
          {},
          createLoaderTree('(..)photo', {}, createLoaderTree('[photoId]'))
        )
      )
      const route = parseNormalizedAppRoute('/gallery/(..)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should extract segment from (...) root-level interception route', () => {
      // Tree: /app/gallery/(...)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'app',
          {},
          createLoaderTree(
            'gallery',
            {},
            createLoaderTree('(...)photo', {}, createLoaderTree('[photoId]'))
          )
        )
      )
      const route = parseNormalizedAppRoute('/app/gallery/(...)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should extract segment from (..)(..) grandparent-level interception route', () => {
      // Tree: /a/b/(..)(..)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'a',
          {},
          createLoaderTree(
            'b',
            {},
            createLoaderTree('(..)(..)photo', {}, createLoaderTree('[photoId]'))
          )
        )
      )
      const route = parseNormalizedAppRoute('/a/b/(..)(..)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should distinguish interception routes from route groups', () => {
      // Tree: /(marketing)/[slug] vs /(.)photo/[photoId]
      const routeGroupTree = createLoaderTree(
        '',
        {},
        createLoaderTree('(marketing)', {}, createLoaderTree('[slug]'))
      )
      const interceptionTree = createLoaderTree(
        '',
        {},
        createLoaderTree('(.)photo', {}, createLoaderTree('[photoId]'))
      )

      const routeGroupRoute = parseNormalizedAppRoute('/[slug]')
      const interceptionRoute = parseNormalizedAppRoute('/(.)photo/[photoId]')

      const { pathnameRouteParamSegments: routeGroupResult } =
        extractPathnameRouteParamSegmentsFromLoaderTree(
          routeGroupTree,
          routeGroupRoute
        )
      const { pathnameRouteParamSegments: interceptionResult } =
        extractPathnameRouteParamSegmentsFromLoaderTree(
          interceptionTree,
          interceptionRoute
        )

      // Route group ignored, slug at depth 0
      expect(routeGroupResult).toEqual([
        { name: '[slug]', paramName: 'slug', paramType: 'dynamic' },
      ])

      // Interception route counts, photoId at depth 1
      expect(interceptionResult).toEqual([
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should handle catchall in interception route', () => {
      // Tree: /(.)photo/[...segments]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('(.)photo', {}, createLoaderTree('[...segments]'))
      )
      const route = parseNormalizedAppRoute('/(.)photo/[...segments]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '[...segments]',
          paramName: 'segments',
          paramType: 'catchall',
        },
      ])
    })

    it('should extract intercepted param when marker is part of the segment itself', () => {
      // Tree: /(.)[photoId] - the interception marker is PART OF the dynamic segment
      // This is the case where -intercepted- types apply (handled by getSegmentParam)
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('(.)[photoId]')
      )
      const route = parseNormalizedAppRoute('/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '(.)[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic-intercepted-(.)', // NOW it has -intercepted- type
        },
      ])
    })
  })

  describe('Interception Routes in Parallel Routes', () => {
    it('should extract segment from interception route in parallel slot', () => {
      // Tree: @modal/(.)photo/[photoId]
      const loaderTree = createLoaderTree('', {
        modal: createLoaderTree('(.)photo', {}, createLoaderTree('[photoId]')),
      })
      const route = parseNormalizedAppRoute('/(.)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should extract segments from both children and intercepting parallel route', () => {
      // Tree: /[id] -> children + @modal/(.)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[id]', {
          modal: createLoaderTree(
            '(.)photo',
            {},
            createLoaderTree('[photoId]')
          ),
        })
      )
      const route = parseNormalizedAppRoute('/[id]/(.)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should extract from multiple parallel routes with interception', () => {
      // Tree: /[category] -> @modal/(.)photo/[photoId] + @sidebar/(.)filter/[filterId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[category]', {
          modal: createLoaderTree(
            '(.)photo',
            {},
            createLoaderTree('[photoId]')
          ),
          sidebar: createLoaderTree(
            '(.)filter',
            {},
            createLoaderTree('[filterId]')
          ),
        })
      )
      const route = parseNormalizedAppRoute('/[category]/(.)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[category]', paramName: 'category', paramType: 'dynamic' },
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should handle (..) interception in parallel route with nested structure', () => {
      // Tree: /gallery/[id] -> @modal/(..)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'gallery',
          {},
          createLoaderTree('[id]', {
            modal: createLoaderTree(
              '(..)photo',
              {},
              createLoaderTree('[photoId]')
            ),
          })
        )
      )
      const route = parseNormalizedAppRoute('/gallery/[id]/(..)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should handle (...) root-level interception in parallel route', () => {
      // Tree: /app/gallery/[id] -> @modal/(...)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'app',
          {},
          createLoaderTree(
            'gallery',
            {},
            createLoaderTree('[id]', {
              modal: createLoaderTree(
                '(...)photo',
                {},
                createLoaderTree('[photoId]')
              ),
            })
          )
        )
      )
      const route = parseNormalizedAppRoute(
        '/app/gallery/[id]/(...)photo/[photoId]'
      )
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should handle catchall in intercepting parallel route', () => {
      // Tree: /[id] -> @modal/(.)details/[...segments]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[id]', {
          modal: createLoaderTree(
            '(.)details',
            {},
            createLoaderTree('[...segments]')
          ),
        })
      )
      const route = parseNormalizedAppRoute('/[id]/(.)details/[...segments]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
        {
          name: '[...segments]',
          paramName: 'segments',
          paramType: 'catchall',
        },
      ])
    })
  })

  describe('Complex Mixed Scenarios', () => {
    it('should handle route groups + parallel routes + interception routes', () => {
      // Tree: /(marketing)/[lang] -> @modal/(.)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '(marketing)',
          {},
          createLoaderTree('[lang]', {
            modal: createLoaderTree(
              '(.)photo',
              {},
              createLoaderTree('[photoId]')
            ),
          })
        )
      )
      const route = parseNormalizedAppRoute('/[lang]/(.)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[lang]', paramName: 'lang', paramType: 'dynamic' },
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should handle deeply nested parallel routes with interception', () => {
      // Tree: /[lang]/blog/[category] -> @modal/(.)post/[slug]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '[lang]',
          {},
          createLoaderTree(
            'blog',
            {},
            createLoaderTree('[category]', {
              modal: createLoaderTree(
                '(.)post',
                {},
                createLoaderTree('[slug]')
              ),
            })
          )
        )
      )
      const route = parseNormalizedAppRoute(
        '/[lang]/blog/[category]/(.)post/[slug]'
      )
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[lang]', paramName: 'lang', paramType: 'dynamic' },
        { name: '[category]', paramName: 'category', paramType: 'dynamic' },
        {
          name: '[slug]',
          paramName: 'slug',
          paramType: 'dynamic',
        },
      ])
    })

    it('should handle multiple interception routes at different levels', () => {
      // Tree: /[id] -> @modal1/(.)a/[a] + @modal2/(..)b/[b]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[id]', {
          modal1: createLoaderTree('(.)a', {}, createLoaderTree('[a]')),
          modal2: createLoaderTree('(..)b', {}, createLoaderTree('[b]')),
        })
      )
      const route = parseNormalizedAppRoute('/[id]/(.)a/[a]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
        {
          name: '[a]',
          paramName: 'a',
          paramType: 'dynamic',
        },
      ])
    })

    it('should extract from actual Next.js photo gallery pattern', () => {
      // Realistic pattern: /photos/[id] with @modal/(.)photo/[photoId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'photos',
          {},
          createLoaderTree('[id]', {
            modal: createLoaderTree(
              '(.)photo',
              {},
              createLoaderTree('[photoId]')
            ),
          })
        )
      )
      const route = parseNormalizedAppRoute('/photos/[id]/(.)photo/[photoId]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
        {
          name: '[photoId]',
          paramName: 'photoId',
          paramType: 'dynamic',
        },
      ])
    })

    it('should handle i18n with interception routes', () => {
      // Tree: /[locale]/products/[category] -> @modal/(.)product/[productId]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '[locale]',
          {},
          createLoaderTree(
            'products',
            {},
            createLoaderTree('[category]', {
              modal: createLoaderTree(
                '(.)product',
                {},
                createLoaderTree('[productId]')
              ),
            })
          )
        )
      )
      const route = parseNormalizedAppRoute(
        '/[locale]/products/[category]/(.)product/[productId]'
      )
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[locale]', paramName: 'locale', paramType: 'dynamic' },
        { name: '[category]', paramName: 'category', paramType: 'dynamic' },
        {
          name: '[productId]',
          paramName: 'productId',
          paramType: 'dynamic',
        },
      ])
    })
  })

  describe('Edge Cases', () => {
    it('should return empty array for pathname with no dynamic segments', () => {
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {}, createLoaderTree('posts'))
      )
      const route = parseNormalizedAppRoute('/blog/posts')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([])
    })

    it('should return empty array when no segments match pathname', () => {
      // Tree has dynamic segments but they don't match the pathname structure
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('api', {}, createLoaderTree('[version]'))
      )
      const route = parseNormalizedAppRoute('/different/path')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([])
    })

    it('should handle empty segment in tree', () => {
      // Tree: '' -> [id]
      const loaderTree = createLoaderTree('', {}, createLoaderTree('[id]'))
      const route = parseNormalizedAppRoute('/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
      ])
    })

    it('should match segments by depth and param name', () => {
      // Tree: /[lang]/blog/[slug] but pathname is /[lang]/[slug]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '[lang]',
          {},
          createLoaderTree('blog', {}, createLoaderTree('[slug]'))
        )
      )
      const route = parseNormalizedAppRoute('/[lang]/[slug]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      // Should match [lang] at depth 0 but not [slug] (wrong depth)
      expect(result).toEqual([
        { name: '[lang]', paramName: 'lang', paramType: 'dynamic' },
      ])
    })

    it('should handle optional catchall in parallel route', () => {
      // Tree: @sidebar/[[...optional]]
      const loaderTree = createLoaderTree('', {
        sidebar: createLoaderTree('[[...optional]]'),
      })
      const route = parseNormalizedAppRoute('/[[...optional]]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        {
          name: '[[...optional]]',
          paramName: 'optional',
          paramType: 'optional-catchall',
        },
      ])
    })

    it('should handle multiple route groups in sequence', () => {
      // Tree: /(a)/(b)/(c)/[id]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '(a)',
          {},
          createLoaderTree(
            '(b)',
            {},
            createLoaderTree('(c)', {}, createLoaderTree('[id]'))
          )
        )
      )
      const route = parseNormalizedAppRoute('/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
      ])
    })
  })

  describe('Static Segment Matching', () => {
    it('should not extract segments when static segments do not match', () => {
      // Tree: /blog/[slug] but pathname is /news/[slug]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {}, createLoaderTree('[slug]'))
      )
      const route = parseNormalizedAppRoute('/news/[slug]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([])
    })

    it('should match when static segments align correctly', () => {
      // Tree: /api/v1/[endpoint] -> /api/v1/[endpoint]
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'api',
          {},
          createLoaderTree('v1', {}, createLoaderTree('[endpoint]'))
        )
      )
      const route = parseNormalizedAppRoute('/api/v1/[endpoint]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([
        { name: '[endpoint]', paramName: 'endpoint', paramType: 'dynamic' },
      ])
    })

    it('should handle segments with values already present in the page', () => {
      // Tree: /blog/[slug] but pathname is /blog/my-slug
      const loaderTree = createLoaderTree(
        '',
        {
          sidebar: createLoaderTree('[[...catchAll]]'),
        },
        createLoaderTree('blog', {}, createLoaderTree('[slug]'))
      )
      const route = parseNormalizedAppRoute('/blog/my-slug')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(result).toEqual([])
    })
  })

  describe('Prefix Validation with Type Mismatch', () => {
    it('should NOT extract param when prefix has type mismatch (static vs dynamic)', () => {
      // Tree: /(.)photo -> @modal/[id]
      // Route: /[category]/[id]
      //
      // When checking @modal/[id] at depth 1:
      //   currentPath = [(.)photo] (STATIC segment)
      //   route.segments[0] = [category] (DYNAMIC segment)
      //   route.segments[1] = [id] (DYNAMIC segment)
      //
      // The [id] param matches at depth 1, BUT the prefix validation should fail
      // because (.)photo (static) doesn't match [category] (dynamic) at depth 0
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('(.)photo', {
          modal: createLoaderTree('[id]'),
        })
      )
      const route = parseNormalizedAppRoute('/[category]/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      // Should return empty array - [id] should NOT be extracted
      // Without the type check, validatePrefixMatch would incorrectly return true
      // because neither the static nor dynamic comparison would trigger
      expect(result).toEqual([])
    })

    it('should NOT extract param when prefix has type mismatch (dynamic vs static)', () => {
      // Tree: /[lang] -> @modal/[id]
      // Route: /photo/[id]
      //
      // When checking @modal/[id] at depth 1:
      //   currentPath = [lang] (DYNAMIC segment)
      //   route.segments[0] = photo (STATIC segment)
      //   route.segments[1] = [id] (DYNAMIC segment)
      //
      // The [id] param matches at depth 1, BUT the prefix validation should fail
      // because [lang] (dynamic) doesn't match photo (static) at depth 0
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[lang]', {
          modal: createLoaderTree('[id]'),
        })
      )
      const route = parseNormalizedAppRoute('/photo/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      // Should return empty array - [id] should NOT be extracted
      // Without the type check, validatePrefixMatch would incorrectly return true
      expect(result).toEqual([])
    })

    it('should extract param when prefix types match correctly', () => {
      // Tree: /blog -> @modal/(.)photo/[id]
      // Route: /blog/(.)photo/[id]
      //
      // When checking @modal/(.)photo/[id]:
      //   currentPath at depth 1 = (.)photo (STATIC segment)
      //   route.segments at depth 1 = (.)photo (STATIC segment)
      //
      // Types match AND names match, so [id] should be extracted
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {
          modal: createLoaderTree('(.)photo', {}, createLoaderTree('[id]')),
        })
      )
      const route = parseNormalizedAppRoute('/blog/(.)photo/[id]')
      const { pathnameRouteParamSegments: result } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      // Should extract [id] because prefix validation succeeds
      expect(result).toEqual([
        {
          name: '[id]',
          paramName: 'id',
          paramType: 'dynamic',
        },
      ])
    })
  })

  describe('Params Resolution', () => {
    it('should resolve single static value for dynamic segment', () => {
      // Tree: /[id]
      // Route: /123 (static value)
      const loaderTree = createLoaderTree('', {}, createLoaderTree('[id]'))
      const route = parseNormalizedAppRoute('/123')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({ id: '123' })
    })

    it('should resolve multiple static values for dynamic segments', () => {
      // Tree: /[category]/[id]
      // Route: /electronics/123
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[category]', {}, createLoaderTree('[id]'))
      )
      const route = parseNormalizedAppRoute('/electronics/123')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({
        category: 'electronics',
        id: '123',
      })
    })

    it('should resolve static value in interception route', () => {
      // Tree: /blog -> @modal/(.)photo/[id]
      // Route: /blog/(.)photo/123
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {
          modal: createLoaderTree('(.)photo', {}, createLoaderTree('[id]')),
        })
      )
      const route = parseNormalizedAppRoute('/blog/(.)photo/123')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({ id: '123' })
    })

    it('should resolve catchall with static segments', () => {
      // Tree: /docs/[...slug]
      // Route: /docs/getting-started/installation
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('docs', {}, createLoaderTree('[...slug]'))
      )
      const route = parseNormalizedAppRoute(
        '/docs/getting-started/installation'
      )
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({
        slug: ['getting-started', 'installation'],
      })
    })

    it('should resolve optional catchall with static segments', () => {
      // Tree: /docs/[[...slug]]
      // Route: /docs/api/reference
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('docs', {}, createLoaderTree('[[...slug]]'))
      )
      const route = parseNormalizedAppRoute('/docs/api/reference')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({
        slug: ['api', 'reference'],
      })
    })

    it('should resolve optional catchall with empty value', () => {
      // Tree: /docs/[[...slug]]
      // Route: /docs
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('docs', {}, createLoaderTree('[[...slug]]'))
      )
      const route = parseNormalizedAppRoute('/docs')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({})
    })

    it('should handle mixed static and dynamic segments', () => {
      // Tree: /blog/[lang]/[slug]
      // Route: /blog/en/[slug] (lang is static, slug is dynamic)
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          'blog',
          {},
          createLoaderTree('[lang]', {}, createLoaderTree('[slug]'))
        )
      )
      const route = parseNormalizedAppRoute('/blog/en/[slug]')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      // [slug] is not in pathnameRouteParamSegments because the prefix has a type
      // mismatch ([lang] dynamic vs 'en' static), so validation fails
      expect(pathnameRouteParamSegments).toEqual([])
      // But lang is still resolved from the static value
      expect(params).toEqual({
        lang: 'en',
      })
    })

    it('should not resolve params when segment is dynamic placeholder', () => {
      // Tree: /[category]/[id]
      // Route: /[category]/[id] (both are placeholders)
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('[category]', {}, createLoaderTree('[id]'))
      )
      const route = parseNormalizedAppRoute('/[category]/[id]')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([
        { name: '[category]', paramName: 'category', paramType: 'dynamic' },
        { name: '[id]', paramName: 'id', paramType: 'dynamic' },
      ])
      expect(params).toEqual({})
    })

    it('should resolve params with route groups', () => {
      // Tree: /(shop)/[category]/[id]
      // Route: /electronics/123
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '(shop)',
          {},
          createLoaderTree('[category]', {}, createLoaderTree('[id]'))
        )
      )
      const route = parseNormalizedAppRoute('/electronics/123')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({
        category: 'electronics',
        id: '123',
      })
    })

    it('should resolve params in parallel routes', () => {
      // Tree: /blog -> @modal/[id]
      // Route: /blog/123 (via parallel route)
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {
          modal: createLoaderTree('[id]'),
        })
      )
      const route = parseNormalizedAppRoute('/blog/123')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({ id: '123' })
    })

    it('should resolve params with interception markers in segment', () => {
      // Tree: /(.)[id]
      // Route: /(.)123
      const loaderTree = createLoaderTree('', {}, createLoaderTree('(.)[id]'))
      const route = parseNormalizedAppRoute('/(.)123')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      // The interception marker is part of the segment name
      expect(params).toEqual({ id: '123' })
    })

    it('should handle catchall with mixed static and dynamic in pathname', () => {
      // Tree: /[...slug]
      // Route: /api/[version]/users (version is dynamic, api and users are static)
      const loaderTree = createLoaderTree('', {}, createLoaderTree('[...slug]'))
      const route = parseNormalizedAppRoute('/api/[version]/users')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      // Should not resolve because pathname contains unknown [version]
      expect(params).toEqual({})
    })

    it('should resolve complex interception route in photo gallery pattern', () => {
      // Tree: / -> @modal/(.)photo/[id]
      // Route: /(.)photo/abc123
      const loaderTree = createLoaderTree('', {
        modal: createLoaderTree('(.)photo', {}, createLoaderTree('[id]')),
      })
      const route = parseNormalizedAppRoute('/(.)photo/abc123')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({ id: 'abc123' })
    })

    it('should resolve params with (..) parent-level interception', () => {
      // Tree: /blog -> @modal/(..)[id]
      // Route: /blog/(..)456
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {
          modal: createLoaderTree('(..)[id]'),
        })
      )
      const route = parseNormalizedAppRoute('/blog/(..)456')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({ id: '456' })
    })

    it('should resolve catch-all params with (..) parent-level interception', () => {
      // Tree: /blog -> @modal/(..)[...catchAll]
      // Route: /blog/(..)some/path/here
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree('blog', {
          modal: createLoaderTree('(..)[...catchAll]'),
        })
      )
      const route = parseNormalizedAppRoute('/blog/(..)some/path/here')
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({ catchAll: ['some', 'path', 'here'] })
    })

    it('should handle deeply nested static values', () => {
      // Tree: /[lang]/[region]/shop/[category]/[id]
      // Route: /en/us/shop/electronics/laptop-123
      const loaderTree = createLoaderTree(
        '',
        {},
        createLoaderTree(
          '[lang]',
          {},
          createLoaderTree(
            '[region]',
            {},
            createLoaderTree(
              'shop',
              {},
              createLoaderTree('[category]', {}, createLoaderTree('[id]'))
            )
          )
        )
      )
      const route = parseNormalizedAppRoute(
        '/en/us/shop/electronics/laptop-123'
      )
      const { pathnameRouteParamSegments, params } =
        extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route)

      expect(pathnameRouteParamSegments).toEqual([])
      expect(params).toEqual({
        lang: 'en',
        region: 'us',
        category: 'electronics',
        id: 'laptop-123',
      })
    })
  })
})
Quest for Codev2.0.0
/
SIGN IN