From 54c8e76a6cf897692aabbe20c15ae95be2a45a53 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 10 Oct 2025 11:10:50 +0200 Subject: [PATCH] test: new resolver and slash encoding This fixes #1638 but only on the new resolver and replaces #2291 with a build version that is lighter and more flexible. --- .../matchers/matcher-pattern.spec.ts | 117 ++++++++++++++++++ .../router/src/experimental/router.spec.ts | 81 +++++++++++- 2 files changed, 194 insertions(+), 4 deletions(-) diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts index 326f5735..35a1d2da 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts @@ -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) => { diff --git a/packages/router/src/experimental/router.spec.ts b/packages/router/src/experimental/router.spec.ts index 96b882a4..e5d8a3dc 100644 --- a/packages/router/src/experimental/router.spec.ts +++ b/packages/router/src/experimental/router.spec.ts @@ -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 () => {}) -- 2.47.3