Skip to content

Commit 9be2b7c

Browse files
committed
fix: hash priority
1 parent 90ec562 commit 9be2b7c

File tree

3 files changed

+254
-1
lines changed

3 files changed

+254
-1
lines changed

packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,62 @@ describe('fixed resolver', () => {
317317
path: '/users/posva/admin',
318318
})
319319
})
320+
321+
it('preserves currentLocation.hash in relative-by-name navigation without to.hash', () => {
322+
const resolver = createFixedResolver([
323+
{
324+
name: 'home',
325+
path: EMPTY_PATH_PATTERN_MATCHER,
326+
hash: ANY_HASH_PATTERN_MATCHER,
327+
},
328+
])
329+
330+
const currentLocation = resolver.resolve('/#current-hash')
331+
332+
expect(resolver.resolve({}, currentLocation)).toMatchObject({
333+
name: 'home',
334+
path: '/',
335+
hash: '#current-hash',
336+
})
337+
})
338+
339+
it('uses currentLocation values when matcher and to values are nullish', () => {
340+
const resolver = createFixedResolver([
341+
{
342+
name: 'page',
343+
path: EMPTY_PATH_PATTERN_MATCHER,
344+
query: [PAGE_QUERY_PATTERN_MATCHER],
345+
hash: ANY_HASH_PATTERN_MATCHER,
346+
},
347+
])
348+
349+
// Create currentLocation using the resolver to ensure it's properly formed
350+
const currentLocation = resolver.resolve({
351+
name: 'page',
352+
params: { page: 10, hash: 'current' },
353+
query: { existing: 'value' },
354+
})
355+
356+
// Verify currentLocation was created correctly
357+
expect(currentLocation).toMatchObject({
358+
name: 'page',
359+
path: '/',
360+
params: { page: 10, hash: 'current' },
361+
query: { existing: 'value', page: '10' }, // matcher adds page to query
362+
hash: '#current', // matcher builds hash from params
363+
fullPath: '/?existing=value&page=10#current',
364+
})
365+
366+
// Now test that relative navigation preserves currentLocation values
367+
expect(resolver.resolve({}, currentLocation)).toMatchObject({
368+
name: 'page',
369+
path: '/',
370+
params: { page: 10, hash: 'current' }, // from currentLocation
371+
query: { existing: 'value', page: '10' }, // matcher builds with currentLocation params
372+
hash: '#current', // matcher builds with currentLocation params
373+
fullPath: '/?existing=value&page=10#current',
374+
})
375+
})
320376
})
321377

322378
describe('absolute locations', () => {
@@ -402,6 +458,200 @@ describe('fixed resolver', () => {
402458
resolver.resolve({ name: 'nonexistent', params: {} })
403459
).toThrowError('Record "nonexistent" not found')
404460
})
461+
462+
it('resolves named locations with explicit query', () => {
463+
const resolver = createFixedResolver([
464+
{
465+
name: 'home',
466+
path: EMPTY_PATH_PATTERN_MATCHER,
467+
},
468+
])
469+
470+
expect(
471+
resolver.resolve({
472+
name: 'home',
473+
params: {},
474+
query: { foo: 'bar', baz: 'qux' },
475+
})
476+
).toMatchObject({
477+
name: 'home',
478+
path: '/',
479+
params: {},
480+
query: { foo: 'bar', baz: 'qux' },
481+
hash: '',
482+
fullPath: '/?foo=bar&baz=qux',
483+
})
484+
})
485+
486+
it('resolves named locations with explicit hash', () => {
487+
const resolver = createFixedResolver([
488+
{
489+
name: 'home',
490+
path: EMPTY_PATH_PATTERN_MATCHER,
491+
},
492+
])
493+
494+
expect(
495+
resolver.resolve({
496+
name: 'home',
497+
params: {},
498+
hash: '#section',
499+
})
500+
).toMatchObject({
501+
name: 'home',
502+
path: '/',
503+
params: {},
504+
query: {},
505+
hash: '#section',
506+
fullPath: '/#section',
507+
})
508+
})
509+
510+
it('resolves named locations with both query and hash', () => {
511+
const resolver = createFixedResolver([
512+
{
513+
name: 'home',
514+
path: EMPTY_PATH_PATTERN_MATCHER,
515+
},
516+
])
517+
518+
expect(
519+
resolver.resolve({
520+
name: 'home',
521+
params: {},
522+
query: { page: '1' },
523+
hash: '#top',
524+
})
525+
).toMatchObject({
526+
name: 'home',
527+
path: '/',
528+
params: {},
529+
query: { page: '1' },
530+
hash: '#top',
531+
fullPath: '/?page=1#top',
532+
})
533+
})
534+
535+
it('resolves named locations with params, query, and hash', () => {
536+
const resolver = createFixedResolver([
537+
{ name: 'user-edit', path: USERS_ID_OTHER_PATH_MATCHER },
538+
])
539+
540+
expect(
541+
resolver.resolve({
542+
name: 'user-edit',
543+
params: { id: 'posva', other: 'profile' },
544+
query: { tab: 'settings' },
545+
hash: '#bio',
546+
})
547+
).toMatchObject({
548+
name: 'user-edit',
549+
path: '/users/posva/profile',
550+
params: { id: 'posva', other: 'profile' },
551+
query: { tab: 'settings' },
552+
hash: '#bio',
553+
fullPath: '/users/posva/profile?tab=settings#bio',
554+
})
555+
})
556+
557+
it('query matcher params take precedence over to.query', () => {
558+
const resolver = createFixedResolver([
559+
{
560+
name: 'search',
561+
path: EMPTY_PATH_PATTERN_MATCHER,
562+
query: [PAGE_QUERY_PATTERN_MATCHER],
563+
},
564+
])
565+
566+
expect(
567+
resolver.resolve({
568+
name: 'search',
569+
params: { page: 42 },
570+
query: { page: '1', other: 'value' },
571+
})
572+
).toMatchObject({
573+
name: 'search',
574+
path: '/',
575+
params: { page: 42 },
576+
query: { page: '42', other: 'value' }, // matcher param overrides to.query
577+
fullPath: '/?page=42&other=value',
578+
})
579+
})
580+
581+
it('hash matcher params take precedence over to.hash', () => {
582+
const resolver = createFixedResolver([
583+
{
584+
name: 'document',
585+
path: EMPTY_PATH_PATTERN_MATCHER,
586+
hash: ANY_HASH_PATTERN_MATCHER,
587+
},
588+
])
589+
590+
expect(
591+
resolver.resolve({
592+
name: 'document',
593+
params: { hash: 'section1' },
594+
hash: '#section2',
595+
})
596+
).toMatchObject({
597+
name: 'document',
598+
path: '/',
599+
params: { hash: 'section1' },
600+
hash: '#section1', // matcher param overrides to.hash
601+
fullPath: '/#section1',
602+
})
603+
})
604+
605+
it('preserves empty string hash from matcher over to.hash', () => {
606+
const resolver = createFixedResolver([
607+
{
608+
name: 'document',
609+
path: EMPTY_PATH_PATTERN_MATCHER,
610+
hash: ANY_HASH_PATTERN_MATCHER,
611+
},
612+
])
613+
614+
expect(
615+
resolver.resolve({
616+
name: 'document',
617+
params: { hash: '' },
618+
hash: '#fallback',
619+
})
620+
).toMatchObject({
621+
name: 'document',
622+
path: '/',
623+
params: { hash: '' },
624+
hash: '', // empty string from matcher is preserved
625+
fullPath: '/',
626+
})
627+
})
628+
629+
it('combines query and hash matchers correctly', () => {
630+
const resolver = createFixedResolver([
631+
{
632+
name: 'page',
633+
path: EMPTY_PATH_PATTERN_MATCHER,
634+
query: [PAGE_QUERY_PATTERN_MATCHER],
635+
hash: ANY_HASH_PATTERN_MATCHER,
636+
},
637+
])
638+
639+
expect(
640+
resolver.resolve({
641+
name: 'page',
642+
params: { page: 5, hash: 'top' },
643+
query: { page: '1', sort: 'name' },
644+
hash: '#bottom',
645+
})
646+
).toMatchObject({
647+
name: 'page',
648+
path: '/',
649+
params: { page: 5, hash: 'top' },
650+
query: { page: '5', sort: 'name' }, // matcher overrides, regular query preserved
651+
hash: '#top', // matcher overrides to.hash
652+
fullPath: '/?page=5&sort=name#top',
653+
})
654+
})
405655
})
406656

407657
describe('encoding', () => {

packages/router/src/experimental/route-resolver/resolver-fixed.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ export function createFixedResolver<
195195
...to.params,
196196
}
197197
const path = record.path.build(params)
198-
const hash = record.hash?.build(params) ?? ''
198+
const hash =
199+
record.hash?.build(params) ?? to.hash ?? currentLocation?.hash ?? ''
199200
const matched = buildMatched(record)
200201
const query = Object.assign(
201202
{

packages/router/src/experimental/router.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,14 @@ describe('Experimental Router', () => {
233233
it('merges meta properties from component-less route records', async () => {
234234
// Create routes that match the original test pattern more closely
235235
const appMainRecord = normalizeRouteRecord({
236+
name: 'app-main',
236237
path: new MatcherPatternPathStatic('/app'),
237238
components: { default: components.Foo },
238239
meta: { parent: true, child: true },
239240
})
240241

241242
const appNestedRecord = normalizeRouteRecord({
243+
name: 'app-nested',
242244
path: new MatcherPatternPathStatic('/app/nested/a/b'),
243245
components: { default: components.Foo },
244246
meta: { parent: true },

0 commit comments

Comments
 (0)