From 9be2b7c89194ba0a351028f8ab64197fb9429d60 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 18 Aug 2025 14:19:16 +0200 Subject: [PATCH] fix: hash priority --- .../route-resolver/resolver-fixed.spec.ts | 250 ++++++++++++++++++ .../route-resolver/resolver-fixed.ts | 3 +- .../router/src/experimental/router.spec.ts | 2 + 3 files changed, 254 insertions(+), 1 deletion(-) diff --git a/packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts b/packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts index 144eae29..1ca16b0d 100644 --- a/packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts +++ b/packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts @@ -317,6 +317,62 @@ describe('fixed resolver', () => { path: '/users/posva/admin', }) }) + + it('preserves currentLocation.hash in relative-by-name navigation without to.hash', () => { + const resolver = createFixedResolver([ + { + name: 'home', + path: EMPTY_PATH_PATTERN_MATCHER, + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) + + const currentLocation = resolver.resolve('/#current-hash') + + expect(resolver.resolve({}, currentLocation)).toMatchObject({ + name: 'home', + path: '/', + hash: '#current-hash', + }) + }) + + it('uses currentLocation values when matcher and to values are nullish', () => { + const resolver = createFixedResolver([ + { + name: 'page', + path: EMPTY_PATH_PATTERN_MATCHER, + query: [PAGE_QUERY_PATTERN_MATCHER], + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) + + // Create currentLocation using the resolver to ensure it's properly formed + const currentLocation = resolver.resolve({ + name: 'page', + params: { page: 10, hash: 'current' }, + query: { existing: 'value' }, + }) + + // Verify currentLocation was created correctly + expect(currentLocation).toMatchObject({ + name: 'page', + path: '/', + params: { page: 10, hash: 'current' }, + query: { existing: 'value', page: '10' }, // matcher adds page to query + hash: '#current', // matcher builds hash from params + fullPath: '/?existing=value&page=10#current', + }) + + // Now test that relative navigation preserves currentLocation values + expect(resolver.resolve({}, currentLocation)).toMatchObject({ + name: 'page', + path: '/', + params: { page: 10, hash: 'current' }, // from currentLocation + query: { existing: 'value', page: '10' }, // matcher builds with currentLocation params + hash: '#current', // matcher builds with currentLocation params + fullPath: '/?existing=value&page=10#current', + }) + }) }) describe('absolute locations', () => { @@ -402,6 +458,200 @@ describe('fixed resolver', () => { resolver.resolve({ name: 'nonexistent', params: {} }) ).toThrowError('Record "nonexistent" not found') }) + + it('resolves named locations with explicit query', () => { + const resolver = createFixedResolver([ + { + name: 'home', + path: EMPTY_PATH_PATTERN_MATCHER, + }, + ]) + + expect( + resolver.resolve({ + name: 'home', + params: {}, + query: { foo: 'bar', baz: 'qux' }, + }) + ).toMatchObject({ + name: 'home', + path: '/', + params: {}, + query: { foo: 'bar', baz: 'qux' }, + hash: '', + fullPath: '/?foo=bar&baz=qux', + }) + }) + + it('resolves named locations with explicit hash', () => { + const resolver = createFixedResolver([ + { + name: 'home', + path: EMPTY_PATH_PATTERN_MATCHER, + }, + ]) + + expect( + resolver.resolve({ + name: 'home', + params: {}, + hash: '#section', + }) + ).toMatchObject({ + name: 'home', + path: '/', + params: {}, + query: {}, + hash: '#section', + fullPath: '/#section', + }) + }) + + it('resolves named locations with both query and hash', () => { + const resolver = createFixedResolver([ + { + name: 'home', + path: EMPTY_PATH_PATTERN_MATCHER, + }, + ]) + + expect( + resolver.resolve({ + name: 'home', + params: {}, + query: { page: '1' }, + hash: '#top', + }) + ).toMatchObject({ + name: 'home', + path: '/', + params: {}, + query: { page: '1' }, + hash: '#top', + fullPath: '/?page=1#top', + }) + }) + + it('resolves named locations with params, query, and hash', () => { + const resolver = createFixedResolver([ + { name: 'user-edit', path: USERS_ID_OTHER_PATH_MATCHER }, + ]) + + expect( + resolver.resolve({ + name: 'user-edit', + params: { id: 'posva', other: 'profile' }, + query: { tab: 'settings' }, + hash: '#bio', + }) + ).toMatchObject({ + name: 'user-edit', + path: '/users/posva/profile', + params: { id: 'posva', other: 'profile' }, + query: { tab: 'settings' }, + hash: '#bio', + fullPath: '/users/posva/profile?tab=settings#bio', + }) + }) + + it('query matcher params take precedence over to.query', () => { + const resolver = createFixedResolver([ + { + name: 'search', + path: EMPTY_PATH_PATTERN_MATCHER, + query: [PAGE_QUERY_PATTERN_MATCHER], + }, + ]) + + expect( + resolver.resolve({ + name: 'search', + params: { page: 42 }, + query: { page: '1', other: 'value' }, + }) + ).toMatchObject({ + name: 'search', + path: '/', + params: { page: 42 }, + query: { page: '42', other: 'value' }, // matcher param overrides to.query + fullPath: '/?page=42&other=value', + }) + }) + + it('hash matcher params take precedence over to.hash', () => { + const resolver = createFixedResolver([ + { + name: 'document', + path: EMPTY_PATH_PATTERN_MATCHER, + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) + + expect( + resolver.resolve({ + name: 'document', + params: { hash: 'section1' }, + hash: '#section2', + }) + ).toMatchObject({ + name: 'document', + path: '/', + params: { hash: 'section1' }, + hash: '#section1', // matcher param overrides to.hash + fullPath: '/#section1', + }) + }) + + it('preserves empty string hash from matcher over to.hash', () => { + const resolver = createFixedResolver([ + { + name: 'document', + path: EMPTY_PATH_PATTERN_MATCHER, + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) + + expect( + resolver.resolve({ + name: 'document', + params: { hash: '' }, + hash: '#fallback', + }) + ).toMatchObject({ + name: 'document', + path: '/', + params: { hash: '' }, + hash: '', // empty string from matcher is preserved + fullPath: '/', + }) + }) + + it('combines query and hash matchers correctly', () => { + const resolver = createFixedResolver([ + { + name: 'page', + path: EMPTY_PATH_PATTERN_MATCHER, + query: [PAGE_QUERY_PATTERN_MATCHER], + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) + + expect( + resolver.resolve({ + name: 'page', + params: { page: 5, hash: 'top' }, + query: { page: '1', sort: 'name' }, + hash: '#bottom', + }) + ).toMatchObject({ + name: 'page', + path: '/', + params: { page: 5, hash: 'top' }, + query: { page: '5', sort: 'name' }, // matcher overrides, regular query preserved + hash: '#top', // matcher overrides to.hash + fullPath: '/?page=5&sort=name#top', + }) + }) }) describe('encoding', () => { diff --git a/packages/router/src/experimental/route-resolver/resolver-fixed.ts b/packages/router/src/experimental/route-resolver/resolver-fixed.ts index b60afd95..0c02c1c5 100644 --- a/packages/router/src/experimental/route-resolver/resolver-fixed.ts +++ b/packages/router/src/experimental/route-resolver/resolver-fixed.ts @@ -195,7 +195,8 @@ export function createFixedResolver< ...to.params, } const path = record.path.build(params) - const hash = record.hash?.build(params) ?? '' + const hash = + record.hash?.build(params) ?? to.hash ?? currentLocation?.hash ?? '' const matched = buildMatched(record) const query = Object.assign( { diff --git a/packages/router/src/experimental/router.spec.ts b/packages/router/src/experimental/router.spec.ts index 915a17ab..0b53cf29 100644 --- a/packages/router/src/experimental/router.spec.ts +++ b/packages/router/src/experimental/router.spec.ts @@ -233,12 +233,14 @@ describe('Experimental Router', () => { it('merges meta properties from component-less route records', async () => { // Create routes that match the original test pattern more closely const appMainRecord = normalizeRouteRecord({ + name: 'app-main', path: new MatcherPatternPathStatic('/app'), components: { default: components.Foo }, meta: { parent: true, child: true }, }) const appNestedRecord = normalizeRouteRecord({ + name: 'app-nested', path: new MatcherPatternPathStatic('/app/nested/a/b'), components: { default: components.Foo }, meta: { parent: true }, -- 2.47.3