next.js/packages/next/src/shared/lib/router/utils/get-dynamic-param.test.ts
get-dynamic-param.test.ts433 lines12.6 KB
import {
  getDynamicParam,
  parseParameter,
  parseMatchedParameter,
  interpolateParallelRouteParams,
} from './get-dynamic-param'
import type { Params } from '../../../../server/request/params'
import { InvariantError } from '../../invariant-error'
import { createMockOpaqueFallbackRouteParams } from '../../../../server/app-render/postponed-state.test'

describe('getDynamicParam', () => {
  describe('basic dynamic parameters (d, di)', () => {
    it('should handle simple string parameter', () => {
      const params: Params = { slug: 'hello-world' }
      const result = getDynamicParam(params, 'slug', 'd', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: 'hello-world',
        type: 'd',
        treeSegment: ['slug', 'hello-world', 'd', null],
      })
    })

    it('should encode special characters in string parameters', () => {
      const params: Params = { slug: 'hello world & stuff' }
      const result = getDynamicParam(params, 'slug', 'd', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: 'hello%20world%20%26%20stuff',
        type: 'd',
        treeSegment: ['slug', 'hello%20world%20%26%20stuff', 'd', null],
      })
    })

    it('should handle unicode characters', () => {
      const params: Params = { slug: 'caf�-na�ve' }
      const result = getDynamicParam(params, 'slug', 'd', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: 'caf%EF%BF%BD-na%EF%BF%BDve',
        type: 'd',
        treeSegment: ['slug', 'caf%EF%BF%BD-na%EF%BF%BDve', 'd', null],
      })
    })

    it('should throw InvariantError for missing dynamic parameter', () => {
      const params: Params = {}

      expect(() => {
        getDynamicParam(params, 'slug', 'd', null, null)
      }).toThrow(InvariantError)
      expect(() => {
        getDynamicParam(params, 'slug', 'd', null, null)
      }).toThrow(
        `Invariant: Missing value for segment key: "slug" with dynamic param type: d. This is a bug in Next.js.`
      )
    })

    it('should throw InvariantError for dynamic intercepted parameter without value', () => {
      const params: Params = {}

      expect(() => {
        getDynamicParam(params, 'slug', 'di(..)(..)', null, null)
      }).toThrow(InvariantError)
      expect(() => {
        getDynamicParam(params, 'slug', 'di(..)(..)', null, null)
      }).toThrow(
        'Invariant: Missing value for segment key: "slug" with dynamic param type: di(..)(..). This is a bug in Next.js.'
      )
    })
  })

  describe('catchall parameters (c, ci)', () => {
    it('should handle array of values for catchall', () => {
      const params: Params = { slug: ['docs', 'getting-started'] }
      const result = getDynamicParam(params, 'slug', 'c', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: ['docs', 'getting-started'],
        type: 'c',
        treeSegment: ['slug', 'docs/getting-started', 'c', null],
      })
    })

    it('should encode array values for catchall', () => {
      const params: Params = { slug: ['docs & guides', 'getting started'] }
      const result = getDynamicParam(params, 'slug', 'c', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: ['docs%20%26%20guides', 'getting%20started'],
        type: 'c',
        treeSegment: [
          'slug',
          'docs%20%26%20guides/getting%20started',
          'c',
          null,
        ],
      })
    })

    it('should handle single string value for catchall', () => {
      const params: Params = { slug: 'single-page' }
      const result = getDynamicParam(params, 'slug', 'c', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: 'single-page',
        type: 'c',
        treeSegment: ['slug', 'single-page', 'c', null],
      })
    })

    it('should handle catchall intercepted (ci) with array values', () => {
      const params: Params = { path: ['photo', '123'] }
      const result = getDynamicParam(params, 'path', 'ci(..)(..)', null, null)

      expect(result).toEqual({
        param: 'path',
        value: ['photo', '123'],
        type: 'ci(..)(..)',
        treeSegment: ['path', 'photo/123', 'ci(..)(..)', null],
      })
    })

    it('should handle parallel routes with fallback params for catchall', () => {
      const params: Params = { category: 'electronics' }
      const fallbackParams = createMockOpaqueFallbackRouteParams({
        slug: ['%%drp:slug:parallel123%%', 'd'],
      })
      const result = getDynamicParam(params, 'slug', 'd', fallbackParams, null)

      expect(result).toEqual({
        param: 'slug',
        value: '%%drp:slug:parallel123%%',
        type: 'd',
        treeSegment: ['slug', '%%drp:slug:parallel123%%', 'd', null],
      })
    })
  })

  describe('optional catchall parameters (oc)', () => {
    it('should handle array of values for optional catchall', () => {
      const params: Params = { slug: ['api', 'users', 'create'] }
      const result = getDynamicParam(params, 'slug', 'oc', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: ['api', 'users', 'create'],
        type: 'oc',
        treeSegment: ['slug', 'api/users/create', 'oc', null],
      })
    })

    it('should return null value for optional catchall without value', () => {
      const params: Params = {}
      const result = getDynamicParam(params, 'slug', 'oc', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: null,
        type: 'oc',
        treeSegment: ['slug', '', 'oc', null],
      })
    })

    it('should encode array values for optional catchall', () => {
      const params: Params = { slug: ['hello world', 'caf�'] }
      const result = getDynamicParam(params, 'slug', 'oc', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: ['hello%20world', 'caf%EF%BF%BD'],
        type: 'oc',
        treeSegment: ['slug', 'hello%20world/caf%EF%BF%BD', 'oc', null],
      })
    })

    it('should handle single string value for optional catchall', () => {
      const params: Params = { slug: 'documentation' }
      const result = getDynamicParam(params, 'slug', 'oc', null, null)

      expect(result).toEqual({
        param: 'slug',
        value: 'documentation',
        type: 'oc',
        treeSegment: ['slug', 'documentation', 'oc', null],
      })
    })
  })

  describe('fallback route parameters', () => {
    it('should use fallback param value when available', () => {
      const params: Params = { slug: 'original-value' }
      const fallbackParams = createMockOpaqueFallbackRouteParams({
        slug: ['%%drp:slug:abc123%%', 'd'],
      })

      const result = getDynamicParam(params, 'slug', 'd', fallbackParams, null)

      expect(result).toEqual({
        param: 'slug',
        value: '%%drp:slug:abc123%%',
        type: 'd',
        treeSegment: ['slug', '%%drp:slug:abc123%%', 'd', null],
      })
    })

    it('should not encode fallback param values', () => {
      const params: Params = { slug: 'hello world' }
      const fallbackParams = createMockOpaqueFallbackRouteParams({
        slug: ['%%drp:slug:xyz789%%', 'd'],
      })

      const result = getDynamicParam(params, 'slug', 'd', fallbackParams, null)

      expect(result.value).toBe('%%drp:slug:xyz789%%')
    })

    it('should use fallback params with catchall routes', () => {
      const params: Params = { slug: ['docs', 'api'] }
      const fallbackParams = createMockOpaqueFallbackRouteParams({
        slug: ['%%drp:slug:def456%%', 'c'],
      })

      const result = getDynamicParam(params, 'slug', 'c', fallbackParams, null)

      expect(result).toEqual({
        param: 'slug',
        value: '%%drp:slug:def456%%',
        type: 'c',
        treeSegment: ['slug', '%%drp:slug:def456%%', 'c', null],
      })
    })

    it('should use fallback params with optional catchall routes', () => {
      const params: Params = {}
      const fallbackParams = createMockOpaqueFallbackRouteParams({
        slug: ['%%drp:slug:ghi789%%', 'oc'],
      })

      const result = getDynamicParam(params, 'slug', 'oc', fallbackParams, null)

      expect(result).toEqual({
        param: 'slug',
        value: '%%drp:slug:ghi789%%',
        type: 'oc',
        treeSegment: ['slug', '%%drp:slug:ghi789%%', 'oc', null],
      })
    })

    it('should fall back to regular encoding when param not in fallback', () => {
      const params: Params = { slug: 'hello world' }
      const fallbackParams = createMockOpaqueFallbackRouteParams({
        other: ['%%drp:other:abc123%%', 'd'],
      })

      const result = getDynamicParam(params, 'slug', 'd', fallbackParams, null)

      expect(result.value).toBe('hello%20world')
    })
  })

  describe('edge cases', () => {
    it('should throw InvariantError for empty string values', () => {
      const params: Params = { slug: '' }

      expect(() => {
        getDynamicParam(params, 'slug', 'd', null, null)
      }).toThrow(InvariantError)
      expect(() => {
        getDynamicParam(params, 'slug', 'd', null, null)
      }).toThrow(
        `Invariant: Missing value for segment key: "slug" with dynamic param type: d. This is a bug in Next.js.`
      )
    })

    it('should handle undefined param values', () => {
      const params: Params = { slug: undefined }

      expect(() => {
        getDynamicParam(params, 'slug', 'd', null, null)
      }).toThrow(InvariantError)
    })
  })
})

describe('parseParameter', () => {
  it('should parse simple dynamic parameter', () => {
    expect(parseParameter('[slug]')).toEqual({
      key: 'slug',
      repeat: false,
      optional: false,
    })
  })

  it('should parse optional parameter', () => {
    expect(parseParameter('[[slug]]')).toEqual({
      key: 'slug',
      repeat: false,
      optional: true,
    })
  })

  it('should parse catchall parameter', () => {
    expect(parseParameter('[...slug]')).toEqual({
      key: 'slug',
      repeat: true,
      optional: false,
    })
  })

  it('should parse optional catchall parameter', () => {
    expect(parseParameter('[[...slug]]')).toEqual({
      key: 'slug',
      repeat: true,
      optional: true,
    })
  })

  it('should parse static segment as non-optional, non-repeat', () => {
    expect(parseParameter('static-page')).toEqual({
      key: 'static-page',
      repeat: false,
      optional: false,
    })
  })

  it('should handle complex parameter names', () => {
    expect(parseParameter('[product-id]')).toEqual({
      key: 'product-id',
      repeat: false,
      optional: false,
    })
  })

  it('should parse parameter with prefix/suffix', () => {
    expect(parseParameter('prefix[slug]suffix')).toEqual({
      key: 'slug',
      repeat: false,
      optional: false,
    })
  })
})

describe('parseMatchedParameter', () => {
  it('should parse matched simple parameter', () => {
    expect(parseMatchedParameter('slug')).toEqual({
      key: 'slug',
      repeat: false,
      optional: false,
    })
  })

  it('should parse matched optional parameter', () => {
    expect(parseMatchedParameter('[slug]')).toEqual({
      key: 'slug',
      repeat: false,
      optional: true,
    })
  })

  it('should parse matched catchall parameter', () => {
    expect(parseMatchedParameter('...slug')).toEqual({
      key: 'slug',
      repeat: true,
      optional: false,
    })
  })

  it('should parse matched optional catchall parameter', () => {
    expect(parseMatchedParameter('[...slug]')).toEqual({
      key: 'slug',
      repeat: true,
      optional: true,
    })
  })

  it('should handle parameter names with special characters', () => {
    expect(parseMatchedParameter('[product_id-123]')).toEqual({
      key: 'product_id-123',
      repeat: false,
      optional: true,
    })
  })
})

// 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('interpolateParallelRouteParams', () => {
  it('should interpolate parallel route params', () => {
    const loaderTree = createLoaderTree(
      '',
      {},
      createLoaderTree(
        'optional-catch-all',
        {
          modal: createLoaderTree('[[...catchAll]]'),
        },
        createLoaderTree('[[...path]]')
      )
    )

    expect(
      interpolateParallelRouteParams(
        loaderTree,
        { path: ['foo', 'bar'] },
        '/optional-catch-all/[[...path]]',
        null
      )
    ).toEqual({ path: ['foo', 'bar'], catchAll: ['foo', 'bar'] })
  })
})
Quest for Codev2.0.0
/
SIGN IN