]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
test: new resolver and slash encoding
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 10 Oct 2025 09:10:50 +0000 (11:10 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Fri, 10 Oct 2025 09:12:12 +0000 (11:12 +0200)
This fixes #1638 but only on the new resolver and replaces #2291 with a
build version that is lighter and more flexible.

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts
packages/router/src/experimental/router.spec.ts

index 326f573577940c9ca7cabf72ab9ba2f4664a2b8f..35a1d2daf105327b420d8371c77c7d9bf3684eda 100644 (file)
@@ -316,6 +316,22 @@ describe('MatcherPatternPathDynamic', () => {
     expect(pattern.build({ teamId: [] })).toBe('/teams/b')
   })
 
+  it('works with empty values for repeatable optional param', () => {
+    const pattern = new MatcherPatternPathDynamic(
+      /^\/teams(?:\/(.+?))?\/b$/i,
+      {
+        teamId: [{}, true, true],
+      },
+      ['teams', 1, 'b']
+    )
+
+    expect(pattern.build({ teamId: '' })).toBe('/teams/b')
+    expect(pattern.build({ teamId: null })).toBe('/teams/b')
+    expect(pattern.build({ teamId: undefined })).toBe('/teams/b')
+    // @ts-expect-error: shouldn't this one be optional
+    expect(pattern.build({})).toBe('/teams/b')
+  })
+
   it('multiple params', () => {
     const pattern = new MatcherPatternPathDynamic(
       /^\/teams\/([^/]+?)\/([^/]+?)$/i,
@@ -439,6 +455,107 @@ describe('MatcherPatternPathDynamic', () => {
     expect(pattern.build({ teamId: [] })).toBe('/teams/')
   })
 
+  it('can have params with slashes in their regex (end)', () => {
+    const pattern = new MatcherPatternPathDynamic(
+      // same as above but with multiple params, some encoded, other not
+      /^\/(lang\/(en|fr))$/i,
+      { p: [] },
+      [0]
+    )
+
+    expect(pattern.match('/lang/en')).toEqual({ p: 'lang/en' })
+    expect(pattern.match('/lang/fr')).toEqual({ p: 'lang/fr' })
+    expect(() => pattern.match('/lang/de')).toThrow()
+    expect(() => pattern.match('/lang/en/')).toThrow()
+
+    expect(pattern.build({ p: 'lang/en' })).toBe('/lang/en')
+    expect(pattern.build({ p: 'lang/fr' })).toBe('/lang/fr')
+    // NOTE: the builder does not validate the param against the regex
+    expect(() => pattern.build({ p: 'lang/de' })).not.toThrow()
+    expect(() => pattern.build({ p: 'lang/fr/' })).not.toThrow()
+  })
+
+  it('can have params with slashes in their regex (middle)', () => {
+    const pattern = new MatcherPatternPathDynamic(
+      // same as above but with multiple params, some encoded, other not
+      /^\/prefix\/(lang\/(en|fr))\/suffix$/i,
+      { p: [] },
+      ['prefix', 0, 'suffix']
+    )
+
+    expect(pattern.match('/prefix/lang/en/suffix')).toEqual({ p: 'lang/en' })
+    expect(pattern.match('/prefix/lang/fr/suffix')).toEqual({ p: 'lang/fr' })
+    expect(() => pattern.match('/prefix/lang/de/suffix')).toThrow()
+    expect(() => pattern.match('/prefix/lang/en/suffix/')).toThrow()
+    expect(() => pattern.match('/prefix/lang/en')).toThrow()
+    expect(() => pattern.match('/lang/en/suffix')).toThrow()
+    expect(() => pattern.match('/prefix//suffix')).toThrow()
+
+    expect(pattern.build({ p: 'lang/en' })).toBe('/prefix/lang/en/suffix')
+    expect(pattern.build({ p: 'lang/fr' })).toBe('/prefix/lang/fr/suffix')
+
+    // NOTE: the builder does not validate the param against the regex
+    // maybe it should
+    expect(() => pattern.build({ p: 'lang/de' })).not.toThrow()
+    expect(() => pattern.build({ p: 'lang/fr/' })).not.toThrow()
+  })
+
+  it('can have a non capturing group in the regex', () => {
+    const pattern = new MatcherPatternPathDynamic(
+      // same as above but with multiple params, some encoded, other not
+      /^\/(?:lang\/(en|fr))$/i,
+      { p: [] },
+      [['lang/', 1]]
+    )
+
+    expect(pattern.match('/lang/en')).toEqual({ p: 'en' })
+    expect(pattern.match('/lang/fr')).toEqual({ p: 'fr' })
+    expect(() => pattern.match('/lang/de')).toThrow()
+
+    expect(pattern.build({ p: 'en' })).toBe('/lang/en')
+    expect(pattern.build({ p: 'fr' })).toBe('/lang/fr')
+  })
+
+  it('can reject invalid param values with a custom param matcher', () => {
+    const pattern = new MatcherPatternPathDynamic(
+      /^\/(lang\/(en|fr))$/i,
+      {
+        p: [
+          {
+            get(value: string) {
+              const v = value.toLowerCase().slice(5 /* 'lang/'.length */)
+              if (v !== 'fr' && v !== 'en') {
+                throw miss()
+              }
+              return v
+            },
+            set(value: 'fr' | 'en') {
+              if (value !== 'fr' && value !== 'en') {
+                throw miss()
+              }
+              return `lang/${value}`
+            },
+          },
+        ],
+      },
+      // we don't encode the slash
+      [0]
+    )
+
+    expect(pattern.match('/lang/en')).toEqual({ p: 'en' })
+    expect(pattern.match('/lang/fr')).toEqual({ p: 'fr' })
+    expect(() => pattern.match('/lang/de')).toThrow()
+
+    expect(pattern.build({ p: 'en' })).toBe('/lang/en')
+    expect(pattern.build({ p: 'fr' })).toBe('/lang/fr')
+    expect(() =>
+      pattern.build({
+        // @ts-expect-error: not valid
+        p: 'de',
+      })
+    ).toThrow()
+  })
+
   describe('custom param parsers', () => {
     const doubleParser = definePathParamParser({
       get: (v: string | null) => {
index 96b882a4a6fadc252f651c55c41b2460379e6317..e5d8a3dcaafacbbdf18bc590b0072904c802155b 100644 (file)
@@ -60,7 +60,7 @@ const paramMatcher = new MatcherPatternPathDynamic(
 
 const optionalMatcher = new MatcherPatternPathDynamic(
   /^\/optional(?:\/([^/]+))?$/,
-  { p: [{}] },
+  { p: [] },
   ['optional', 1]
 )
 
@@ -71,9 +71,10 @@ const repeatMatcher = new MatcherPatternPathDynamic(
 )
 
 const catchAllMatcher = new MatcherPatternPathDynamic(
-  /^\/(.*)$/,
-  { pathMatch: [{}, true] },
-  [0]
+  /^\/(.*)$/i,
+  { pathMatch: [] },
+  [0],
+  null
 )
 
 // Create experimental route records using proper structure
@@ -141,6 +142,27 @@ const routeRecords: EXPERIMENTAL_RouteRecord_Matchable[] = [
     path: catchAllMatcher,
     components: { default: components.Home },
   },
+  {
+    name: 'param-with-slashes',
+    path: new MatcherPatternPathDynamic(
+      // https://github.com/vuejs/router/issues/1638
+      // we should be able to keep slashes in params
+      /^\/(lang\/(en|fr))$/i,
+      { p: [] },
+      [0]
+    ),
+    components: { default: components.Foo },
+  },
+  {
+    name: 'mixed-param-with-slashes',
+    path: new MatcherPatternPathDynamic(
+      // same as above but with multiple params, some encoded, other not
+      /^\/(lang\/(en|fr))$/i,
+      { p: [] },
+      [0]
+    ),
+    components: { default: components.Foo },
+  },
 ]
 
 // Normalize all records
@@ -456,6 +478,57 @@ describe('Experimental Router', () => {
     })
   })
 
+  it('keeps slashes in star params', async () => {
+    const { router } = await newRouter()
+
+    expect(
+      router.resolve({
+        name: 'catch-all',
+        params: { pathMatch: 'some/path/with/slashes' },
+        query: { a: '1' },
+        hash: '#hash',
+      })
+    ).toMatchObject({
+      fullPath: '/some/path/with/slashes?a=1#hash',
+      path: '/some/path/with/slashes',
+      query: { a: '1' },
+      hash: '#hash',
+    })
+  })
+
+  it('keeps slashes in params containing slashes', async () => {
+    const { router } = await newRouter()
+
+    expect(
+      router.resolve({ name: 'param-with-slashes', params: { p: 'lang/en' } })
+    ).toMatchObject({
+      fullPath: '/lang/en',
+      path: '/lang/en',
+      params: { p: 'lang/en' },
+    })
+
+    expect(() =>
+      router.resolve({ name: 'param-with-slashes', params: { p: 'lang/es' } })
+    ).toThrowError()
+    expect(() =>
+      router.resolve({
+        name: 'param-with-slashes',
+        params: { p: 'lang/fr/nope' },
+      })
+    ).toThrowError()
+    // NOTE: this version of the matcher is not strict on the trailing slash
+    expect(
+      router.resolve({
+        name: 'param-with-slashes',
+        params: { p: 'lang/fr/' },
+      })
+    ).toMatchObject({
+      fullPath: '/lang/fr',
+      path: '/lang/fr',
+      params: { p: 'lang/fr' },
+    })
+  })
+
   it.skip('can pass a currentLocation to resolve', async () => {})
 
   it.skip('resolves relative locations', async () => {})