Skip to content

Commit bd034bd

Browse files
fix(router-core): buildLocation must consider basePath when resolving fromPath (#4930)
when building the location we are already considering the `basepath` for the `to` path resolution but we don't do the same for the `from` path. This PR resolves the `from` path also with the `basepath` so as to ensure a like for like comparison with `to`. This PR resolves #4924 and also adds a test to check for this regression going forward --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent cb895ff commit bd034bd

File tree

2 files changed

+115
-1
lines changed

2 files changed

+115
-1
lines changed

packages/react-router/tests/link.test.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,118 @@ describe('Link', () => {
751751
expect(updatedFilter).toHaveTextContent('Filter: inactive')
752752
})
753753

754+
test('when navigation to . from /posts while updating search from / and using base path', async () => {
755+
const RootComponent = () => {
756+
return (
757+
<>
758+
<div data-testid="root-nav">
759+
<Link
760+
to="."
761+
search={{ page: 2, filter: 'inactive' }}
762+
data-testid="update-search"
763+
>
764+
Update Search
765+
</Link>
766+
</div>
767+
<Outlet />
768+
</>
769+
)
770+
}
771+
772+
const rootRoute = createRootRoute({
773+
component: RootComponent,
774+
})
775+
776+
const indexRoute = createRoute({
777+
getParentRoute: () => rootRoute,
778+
path: '/',
779+
component: () => {
780+
return (
781+
<>
782+
<h1>Index</h1>
783+
<Link
784+
to="/posts"
785+
search={{ page: 1, filter: 'active' }}
786+
data-testid="to-posts"
787+
>
788+
Go to Posts
789+
</Link>
790+
</>
791+
)
792+
},
793+
})
794+
795+
const PostsComponent = () => {
796+
const search = useSearch({ strict: false })
797+
return (
798+
<>
799+
<h1>Posts</h1>
800+
<span data-testid="current-page">Page: {search.page}</span>
801+
<span data-testid="current-filter">Filter: {search.filter}</span>
802+
</>
803+
)
804+
}
805+
806+
const postsRoute = createRoute({
807+
getParentRoute: () => rootRoute,
808+
path: 'posts',
809+
validateSearch: (input: Record<string, unknown>) => {
810+
return {
811+
page: input.page ? Number(input.page) : 1,
812+
filter: (input.filter as string) || 'all',
813+
}
814+
},
815+
component: PostsComponent,
816+
})
817+
818+
const router = createRouter({
819+
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
820+
history,
821+
})
822+
823+
render(<RouterProvider router={router} basepath={'/Dashboard'} />)
824+
825+
// Start at index page
826+
const toPostsLink = await screen.findByTestId('to-posts')
827+
expect(toPostsLink).toHaveAttribute(
828+
'href',
829+
'/Dashboard/posts?page=1&filter=active',
830+
)
831+
832+
// Navigate to posts with initial search params
833+
await act(() => fireEvent.click(toPostsLink))
834+
835+
// Verify we're on posts with initial search
836+
const postsHeading = await screen.findByRole('heading', { name: 'Posts' })
837+
expect(postsHeading).toBeInTheDocument()
838+
expect(window.location.pathname).toBe('/Dashboard/posts')
839+
expect(window.location.search).toBe('?page=1&filter=active')
840+
841+
const currentPage = await screen.findByTestId('current-page')
842+
const currentFilter = await screen.findByTestId('current-filter')
843+
expect(currentPage).toHaveTextContent('Page: 1')
844+
expect(currentFilter).toHaveTextContent('Filter: active')
845+
846+
// Navigate to current route (.) with updated search
847+
const updateSearchLink = await screen.findByTestId('update-search')
848+
849+
expect(updateSearchLink).toHaveAttribute(
850+
'href',
851+
'/Dashboard/posts?page=2&filter=inactive',
852+
)
853+
854+
await act(() => fireEvent.click(updateSearchLink))
855+
856+
// Verify search was updated
857+
expect(window.location.pathname).toBe('/Dashboard/posts')
858+
expect(window.location.search).toBe('?page=2&filter=inactive')
859+
860+
const updatedPage = await screen.findByTestId('current-page')
861+
const updatedFilter = await screen.findByTestId('current-filter')
862+
expect(updatedPage).toHaveTextContent('Page: 2')
863+
expect(updatedFilter).toHaveTextContent('Filter: inactive')
864+
})
865+
754866
test('when navigating to /posts with invalid search', async () => {
755867
const rootRoute = createRootRoute()
756868
const onError = vi.fn()

packages/router-core/src/router.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1420,7 +1420,7 @@ export class RouterCore<
14201420

14211421
// First let's find the starting pathname
14221422
// By default, start with the current location
1423-
let fromPath = lastMatch.fullPath
1423+
let fromPath = this.resolvePathWithBase(lastMatch.fullPath, '.')
14241424
const toPath = dest.to
14251425
? this.resolvePathWithBase(fromPath, `${dest.to}`)
14261426
: this.resolvePathWithBase(fromPath, '.')
@@ -1461,6 +1461,8 @@ export class RouterCore<
14611461
}
14621462
}
14631463

1464+
fromPath = this.resolvePathWithBase(fromPath, '.')
1465+
14641466
// From search should always use the current location
14651467
const fromSearch = lastMatch.search
14661468
// Same with params. It can't hurt to provide as many as possible

0 commit comments

Comments
 (0)