From 1c537f79902a94a52bfdb6ee8779bb4cc377cb74 Mon Sep 17 00:00:00 2001 From: Nico Lynzaad Date: Sun, 6 Jul 2025 02:48:46 +0200 Subject: [PATCH 1/8] make allowance for trailing slashes --- packages/router-core/src/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index a2b9a921164..084ce550d89 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1428,7 +1428,7 @@ export class RouterCore< const routeIsChanging = !!dest.to && dest.to !== fromPath && - this.resolvePathWithBase(fromPath, `${dest.to}`) !== fromPath + joinPaths([this.resolvePathWithBase(fromPath, `${dest.to}`), "/"]) !== joinPaths([fromPath, "/"]); // If the route is changing we need to find the relative fromPath if (dest.unsafeRelative === 'path') { From caa5353b4555947ee70307960098cbe93de09668 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 6 Jul 2025 00:59:31 +0000 Subject: [PATCH 2/8] ci: apply automated fixes --- packages/router-core/src/router.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 084ce550d89..d3faeb73aed 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1428,7 +1428,8 @@ export class RouterCore< const routeIsChanging = !!dest.to && dest.to !== fromPath && - joinPaths([this.resolvePathWithBase(fromPath, `${dest.to}`), "/"]) !== joinPaths([fromPath, "/"]); + joinPaths([this.resolvePathWithBase(fromPath, `${dest.to}`), '/']) !== + joinPaths([fromPath, '/']) // If the route is changing we need to find the relative fromPath if (dest.unsafeRelative === 'path') { From 3bea2fe4be65f5f156dffe1af6a28193f9fcb93d Mon Sep 17 00:00:00 2001 From: Nico Lynzaad Date: Sun, 6 Jul 2025 13:15:57 +0200 Subject: [PATCH 3/8] update test --- .../solid-router/tests/ClientOnly.test.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/solid-router/tests/ClientOnly.test.tsx b/packages/solid-router/tests/ClientOnly.test.tsx index 48c8032e9dd..fba3fbf8f83 100644 --- a/packages/solid-router/tests/ClientOnly.test.tsx +++ b/packages/solid-router/tests/ClientOnly.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { renderToString } from 'solid-js/web' import { cleanup, render, screen } from '@solidjs/testing-library' import { @@ -45,6 +45,15 @@ function createTestRouter(initialHistory?: RouterHistory) { } describe('ClientOnly', () => { + beforeEach(() => { + window.scrollTo = vi.fn(); + }); + + // Clear mocks after each test to prevent interference + afterEach(() => { + vi.clearAllMocks(); + }); + it.skip('should render fallback during SSR', async () => { const { router } = createTestRouter() await router.load() @@ -65,8 +74,8 @@ describe('ClientOnly', () => { render(() => ) - expect(screen.getByText('Client Only Content')).toBeInTheDocument() - expect(screen.queryByText('Loading...')).not.toBeInTheDocument() + expect(await screen.findByTestId('client-only-content')).toBeInTheDocument() + expect(screen.queryByTestId('loading')).not.toBeInTheDocument() }) it('should handle navigation with client-only content', async () => { @@ -79,11 +88,14 @@ describe('ClientOnly', () => { // Re-render after hydration render(() => ) + // Content should be visible before navigation + expect(await screen.findByTestId('client-only-content')).toBeInTheDocument() + // Navigate to a different route and back await router.navigate({ to: '/other' }) await router.navigate({ to: '/' }) // Content should still be visible after navigation - expect(screen.getByText('Client Only Content')).toBeInTheDocument() + expect(await screen.findByTestId('client-only-content')).toBeInTheDocument() }) }) From 992028de9484c0fea82dfca0fcf98d5f56f6dbdb Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 6 Jul 2025 11:17:20 +0000 Subject: [PATCH 4/8] ci: apply automated fixes --- packages/solid-router/tests/ClientOnly.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/solid-router/tests/ClientOnly.test.tsx b/packages/solid-router/tests/ClientOnly.test.tsx index fba3fbf8f83..ad20480a850 100644 --- a/packages/solid-router/tests/ClientOnly.test.tsx +++ b/packages/solid-router/tests/ClientOnly.test.tsx @@ -46,13 +46,13 @@ function createTestRouter(initialHistory?: RouterHistory) { describe('ClientOnly', () => { beforeEach(() => { - window.scrollTo = vi.fn(); - }); + window.scrollTo = vi.fn() + }) // Clear mocks after each test to prevent interference afterEach(() => { - vi.clearAllMocks(); - }); + vi.clearAllMocks() + }) it.skip('should render fallback during SSR', async () => { const { router } = createTestRouter() From b46f90fd89557dc2adedac3cb1a535d6462fdb95 Mon Sep 17 00:00:00 2001 From: Nico Lynzaad Date: Sun, 6 Jul 2025 15:06:35 +0200 Subject: [PATCH 5/8] add test for trailing slashes --- .../react-router/tests/useNavigate.test.tsx | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx index e6bc45a5f90..9c6a9e418ec 100644 --- a/packages/react-router/tests/useNavigate.test.tsx +++ b/packages/react-router/tests/useNavigate.test.tsx @@ -1580,6 +1580,165 @@ test('should navigate to current route with changing path params when using "." expect(window.location.pathname).toEqual('/posts/id2') }) +test('trailing slashes should not break "." navigation', async () => { + const rootRoute = createRootRoute() + + const IndexComponent = () => { + const navigate = useNavigate() + return ( + <> +

Index

+ + + ) + } + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, + }) + + const PostsComponent = () => { + const navigate = postsRoute.useNavigate() + return ( + <> +

Posts

+ + + + ) + } + + const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + component: PostsComponent, + }) + + const useModal = (name: string) => { + const currentOpen = postRoute.useSearch({ + select: (search) => search[`_${name}`], + }); + + const navigate = useNavigate(); + + const setModal = React.useCallback( + (open: boolean) => { + navigate({ + to: ".", + search: (prev: { }) => ({ + ...prev, + [`_${name}`]: open ? true : undefined, + }), + resetScroll: false, + }); + }, + [name, navigate], + ); + + return [currentOpen, setModal] as const; + } + + function DetailComponent(props: {id: string}) { + const params = useParams({strict: false}) + const [currentTest, setTest] = useModal("test") + + return <> +
Post Path "/{params.postId}/detail-{props.id}"!
+ {currentTest + ? + : } + + } + + const PostComponent = () => { + const params = useParams({strict: false}) + + return ( +
+
Post "{params.postId}"!
+ + +
+ ) + } + + const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '$postId', + component: PostComponent, + validateSearch: z.object({ + _test: z.boolean().optional(), + }), + }) + + const detailRoute = createRoute({ + getParentRoute: () => postRoute, + path: 'detail/', + component: () => , + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([ + indexRoute, + postsRoute.addChildren([ + postRoute.addChildren([detailRoute]) + ]) + ]), + }) + + render() + + const postsButton = await screen.findByTestId('posts-btn') + + fireEvent.click(postsButton) + + expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument() + + const post1Button = await screen.findByTestId('first-post-btn') + + fireEvent.click(post1Button) + expect(await screen.findByTestId('post-heading')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-1')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-2')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-1')).toHaveTextContent('Post Path "/id1/detail-1') + expect(await screen.findByTestId('detail-heading-2')).toHaveTextContent('Post Path "/id1/detail-2') + + const detail1AddBtn = await screen.findByTestId('detail-btn-add-1') + + fireEvent.click(detail1AddBtn) + + expect(router.state.location.pathname).toBe('/posts/id1/detail') + expect(router.state.location.search).toEqual({ _test: true }) + + const detail1RemoveBtn = await screen.findByTestId('detail-btn-remove-1') + + fireEvent.click(detail1RemoveBtn) + + expect(router.state.location.pathname).toBe('/posts/id1/detail') + expect(router.state.location.search).toEqual({ }) + + const detail2AddBtn = await screen.findByTestId('detail-btn-add-2') + + fireEvent.click(detail2AddBtn) + + expect(router.state.location.pathname).toBe('/posts/id1/detail') + expect(router.state.location.search).toEqual({ _test: true }) +}) + describe('when on /posts/$postId and navigating to ../ with default `from` /posts', () => { async function runTest(navigateVia: 'Route' | 'RouteApi') { const rootRoute = createRootRoute() From 08e8c32c20ac2aeaad0810c6d8894773c2877a6a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 6 Jul 2025 13:08:50 +0000 Subject: [PATCH 6/8] ci: apply automated fixes --- .../react-router/tests/useNavigate.test.tsx | 273 ++++++++++-------- 1 file changed, 147 insertions(+), 126 deletions(-) diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx index 9c6a9e418ec..274a8c72e78 100644 --- a/packages/react-router/tests/useNavigate.test.tsx +++ b/packages/react-router/tests/useNavigate.test.tsx @@ -1581,162 +1581,183 @@ test('should navigate to current route with changing path params when using "." }) test('trailing slashes should not break "." navigation', async () => { - const rootRoute = createRootRoute() + const rootRoute = createRootRoute() - const IndexComponent = () => { - const navigate = useNavigate() - return ( - <> -

Index

- - - ) - } + const IndexComponent = () => { + const navigate = useNavigate() + return ( + <> +

Index

+ + + ) + } - const indexRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/', - component: IndexComponent, - }) + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, + }) - const PostsComponent = () => { - const navigate = postsRoute.useNavigate() - return ( - <> -

Posts

- - - - ) - } + const PostsComponent = () => { + const navigate = postsRoute.useNavigate() + return ( + <> +

Posts

+ + + + ) + } - const postsRoute = createRoute({ - getParentRoute: () => rootRoute, - path: 'posts', - component: PostsComponent, + const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + component: PostsComponent, + }) + + const useModal = (name: string) => { + const currentOpen = postRoute.useSearch({ + select: (search) => search[`_${name}`], }) - const useModal = (name: string) => { - const currentOpen = postRoute.useSearch({ - select: (search) => search[`_${name}`], - }); - - const navigate = useNavigate(); - - const setModal = React.useCallback( - (open: boolean) => { - navigate({ - to: ".", - search: (prev: { }) => ({ - ...prev, - [`_${name}`]: open ? true : undefined, - }), - resetScroll: false, - }); - }, - [name, navigate], - ); + const navigate = useNavigate() - return [currentOpen, setModal] as const; - } + const setModal = React.useCallback( + (open: boolean) => { + navigate({ + to: '.', + search: (prev: {}) => ({ + ...prev, + [`_${name}`]: open ? true : undefined, + }), + resetScroll: false, + }) + }, + [name, navigate], + ) - function DetailComponent(props: {id: string}) { - const params = useParams({strict: false}) - const [currentTest, setTest] = useModal("test") + return [currentOpen, setModal] as const + } - return <> -
Post Path "/{params.postId}/detail-{props.id}"!
- {currentTest - ? - : } + function DetailComponent(props: { id: string }) { + const params = useParams({ strict: false }) + const [currentTest, setTest] = useModal('test') + + return ( + <> +
+ Post Path "/{params.postId}/detail-{props.id}"! +
+ {currentTest ? ( + + ) : ( + + )} - } + ) + } - const PostComponent = () => { - const params = useParams({strict: false}) + const PostComponent = () => { + const params = useParams({ strict: false }) - return ( -
-
Post "{params.postId}"!
- - -
- ) - } + return ( +
+
Post "{params.postId}"!
+ + +
+ ) + } - const postRoute = createRoute({ - getParentRoute: () => postsRoute, - path: '$postId', - component: PostComponent, - validateSearch: z.object({ - _test: z.boolean().optional(), - }), - }) + const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '$postId', + component: PostComponent, + validateSearch: z.object({ + _test: z.boolean().optional(), + }), + }) - const detailRoute = createRoute({ - getParentRoute: () => postRoute, - path: 'detail/', - component: () => , - }) + const detailRoute = createRoute({ + getParentRoute: () => postRoute, + path: 'detail/', + component: () => , + }) - const router = createRouter({ - routeTree: rootRoute.addChildren([ - indexRoute, - postsRoute.addChildren([ - postRoute.addChildren([detailRoute]) - ]) - ]), - }) + const router = createRouter({ + routeTree: rootRoute.addChildren([ + indexRoute, + postsRoute.addChildren([postRoute.addChildren([detailRoute])]), + ]), + }) - render() + render() - const postsButton = await screen.findByTestId('posts-btn') + const postsButton = await screen.findByTestId('posts-btn') - fireEvent.click(postsButton) + fireEvent.click(postsButton) - expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument() + expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument() - const post1Button = await screen.findByTestId('first-post-btn') + const post1Button = await screen.findByTestId('first-post-btn') - fireEvent.click(post1Button) - expect(await screen.findByTestId('post-heading')).toBeInTheDocument() - expect(await screen.findByTestId('detail-heading-1')).toBeInTheDocument() - expect(await screen.findByTestId('detail-heading-2')).toBeInTheDocument() - expect(await screen.findByTestId('detail-heading-1')).toHaveTextContent('Post Path "/id1/detail-1') - expect(await screen.findByTestId('detail-heading-2')).toHaveTextContent('Post Path "/id1/detail-2') + fireEvent.click(post1Button) + expect(await screen.findByTestId('post-heading')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-1')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-2')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-1')).toHaveTextContent( + 'Post Path "/id1/detail-1', + ) + expect(await screen.findByTestId('detail-heading-2')).toHaveTextContent( + 'Post Path "/id1/detail-2', + ) - const detail1AddBtn = await screen.findByTestId('detail-btn-add-1') + const detail1AddBtn = await screen.findByTestId('detail-btn-add-1') - fireEvent.click(detail1AddBtn) + fireEvent.click(detail1AddBtn) - expect(router.state.location.pathname).toBe('/posts/id1/detail') - expect(router.state.location.search).toEqual({ _test: true }) + expect(router.state.location.pathname).toBe('/posts/id1/detail') + expect(router.state.location.search).toEqual({ _test: true }) - const detail1RemoveBtn = await screen.findByTestId('detail-btn-remove-1') + const detail1RemoveBtn = await screen.findByTestId('detail-btn-remove-1') - fireEvent.click(detail1RemoveBtn) + fireEvent.click(detail1RemoveBtn) - expect(router.state.location.pathname).toBe('/posts/id1/detail') - expect(router.state.location.search).toEqual({ }) + expect(router.state.location.pathname).toBe('/posts/id1/detail') + expect(router.state.location.search).toEqual({}) - const detail2AddBtn = await screen.findByTestId('detail-btn-add-2') + const detail2AddBtn = await screen.findByTestId('detail-btn-add-2') - fireEvent.click(detail2AddBtn) + fireEvent.click(detail2AddBtn) - expect(router.state.location.pathname).toBe('/posts/id1/detail') - expect(router.state.location.search).toEqual({ _test: true }) + expect(router.state.location.pathname).toBe('/posts/id1/detail') + expect(router.state.location.search).toEqual({ _test: true }) }) describe('when on /posts/$postId and navigating to ../ with default `from` /posts', () => { From 10f27a8d45e6c24184dcef00525d829caa1e344d Mon Sep 17 00:00:00 2001 From: Nico Lynzaad Date: Sun, 6 Jul 2025 17:47:42 +0200 Subject: [PATCH 7/8] enhance comparison and tests --- .../react-router/tests/useNavigate.test.tsx | 442 +++++++++--------- packages/router-core/src/router.ts | 18 +- 2 files changed, 225 insertions(+), 235 deletions(-) diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx index 274a8c72e78..0260c6bfb26 100644 --- a/packages/react-router/tests/useNavigate.test.tsx +++ b/packages/react-router/tests/useNavigate.test.tsx @@ -1366,98 +1366,102 @@ test(' navigates only once in ', async () => { expect(navigateSpy.mock.calls.length).toBe(1) }) -test('should navigate to current route with search params when using "." in nested route structure', async () => { - const rootRoute = createRootRoute() +test.each([true,false])('should navigate to current route with search params when using "." in nested route structure from Index Route', async (trailingSlash: boolean) => { + const tail = trailingSlash ? '/' : ''; - const IndexComponent = () => { - const navigate = useNavigate() - return ( - <> - - - - - - ) - } + const rootRoute = createRootRoute() - const indexRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/', - component: IndexComponent, - validateSearch: z.object({ - param1: z.string().optional(), - }), - }) + const IndexComponent = () => { + const navigate = useNavigate() + return ( + <> + + + + + + ) + } - const postRoute = createRoute({ - getParentRoute: () => indexRoute, - path: 'post', - component: () =>
Post
, - }) + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, + validateSearch: z.object({ + param1: z.string().optional(), + }), + }) - const router = createRouter({ - routeTree: rootRoute.addChildren([indexRoute, postRoute]), - history, - }) + const postRoute = createRoute({ + getParentRoute: () => indexRoute, + path: 'post', + component: () =>
Post
, + }) - render() + const router = createRouter({ + routeTree: rootRoute.addChildren([indexRoute, postRoute]), + history, + trailingSlash: trailingSlash ? 'always' : 'never', + }) - const postButton = await screen.findByTestId('posts-btn') + render() - fireEvent.click(postButton) + const postButton = await screen.findByTestId('posts-btn') - expect(router.state.location.pathname).toBe('/post') + fireEvent.click(postButton) - const searchButton = await screen.findByTestId('search-btn') + expect(router.state.location.pathname).toBe(`/post${tail}`) - fireEvent.click(searchButton) + const searchButton = await screen.findByTestId('search-btn') - expect(router.state.location.pathname).toBe('/post') - expect(router.state.location.search).toEqual({ param1: 'value1' }) + fireEvent.click(searchButton) - const searchButton2 = await screen.findByTestId('search2-btn') + expect(router.state.location.pathname).toBe(`/post${tail}`) + expect(router.state.location.search).toEqual({ param1: 'value1' }) - fireEvent.click(searchButton2) + const searchButton2 = await screen.findByTestId('search2-btn') - expect(router.state.location.pathname).toBe('/post') - expect(router.state.location.search).toEqual({ param1: 'value2' }) -}) + fireEvent.click(searchButton2) + + expect(router.state.location.pathname).toBe(`/post${tail}`) + expect(router.state.location.search).toEqual({ param1: 'value2' }) + }) -test('should navigate to current route with changing path params when using "." in nested route structure', async () => { +test.each([true,false])('should navigate to current route with changing path params when using "." in nested route structure', async (trailingSlash) => { + const tail = trailingSlash ? '/' : ''; const rootRoute = createRootRoute() const IndexComponent = () => { @@ -1554,6 +1558,7 @@ test('should navigate to current route with changing path params when using "." indexRoute, layoutRoute.addChildren([postsRoute.addChildren([postRoute])]), ]), + trailingSlash: trailingSlash ? 'always' : 'never', }) render() @@ -1563,201 +1568,182 @@ test('should navigate to current route with changing path params when using "." fireEvent.click(postsButton) expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument() - expect(window.location.pathname).toEqual('/posts') + expect(window.location.pathname).toEqual(`/posts${tail}`) const firstPostButton = await screen.findByTestId('first-post-btn') fireEvent.click(firstPostButton) expect(await screen.findByTestId('post-id1')).toBeInTheDocument() - expect(window.location.pathname).toEqual('/posts/id1') + expect(window.location.pathname).toEqual(`/posts/id1${tail}`) const secondPostButton = await screen.findByTestId('second-post-btn') fireEvent.click(secondPostButton) expect(await screen.findByTestId('post-id2')).toBeInTheDocument() - expect(window.location.pathname).toEqual('/posts/id2') + expect(window.location.pathname).toEqual(`/posts/id2${tail}`) }) -test('trailing slashes should not break "." navigation', async () => { - const rootRoute = createRootRoute() - - const IndexComponent = () => { - const navigate = useNavigate() - return ( - <> -

Index

- - - ) - } +test.each([true,false])('should navigate to current route with search params when using "." in nested route structure from non-Index Route', async (trailingSlash) => { + const tail = trailingSlash ? '/' : ''; + const rootRoute = createRootRoute() - const indexRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/', - component: IndexComponent, - }) + const IndexComponent = () => { + const navigate = useNavigate() + return ( + <> +

Index

+ + + ) + } - const PostsComponent = () => { - const navigate = postsRoute.useNavigate() - return ( - <> -

Posts

- - - - ) - } + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, + }) - const postsRoute = createRoute({ - getParentRoute: () => rootRoute, - path: 'posts', - component: PostsComponent, - }) + const PostsComponent = () => { + const navigate = postsRoute.useNavigate() + return ( + <> +

Posts

+ + + + ) + } - const useModal = (name: string) => { - const currentOpen = postRoute.useSearch({ - select: (search) => search[`_${name}`], + const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + component: PostsComponent, }) - const navigate = useNavigate() - - const setModal = React.useCallback( - (open: boolean) => { - navigate({ - to: '.', - search: (prev: {}) => ({ - ...prev, - [`_${name}`]: open ? true : undefined, - }), - resetScroll: false, - }) - }, - [name, navigate], - ) + const useModal = (name: string) => { + const currentOpen = postRoute.useSearch({ + select: (search) => search[`_${name}`], + }); + + const navigate = useNavigate(); + + const setModal = React.useCallback( + (open: boolean) => { + navigate({ + to: ".", + search: (prev: { }) => ({ + ...prev, + [`_${name}`]: open ? true : undefined, + }), + resetScroll: false, + }); + }, + [name, navigate], + ); - return [currentOpen, setModal] as const - } + return [currentOpen, setModal] as const; + } - function DetailComponent(props: { id: string }) { - const params = useParams({ strict: false }) - const [currentTest, setTest] = useModal('test') + function DetailComponent(props: {id: string}) { + const params = useParams({strict: false}) + const [currentTest, setTest] = useModal("test") - return ( - <> -
- Post Path "/{params.postId}/detail-{props.id}"! -
- {currentTest ? ( - - ) : ( - - )} + return <> +
Post Path "/{params.postId}/detail-{props.id}"!
+ {currentTest + ? + : } - ) - } + } - const PostComponent = () => { - const params = useParams({ strict: false }) + const PostComponent = () => { + const params = useParams({strict: false}) - return ( -
-
Post "{params.postId}"!
- - -
- ) - } + return ( +
+
Post "{params.postId}"!
+ + +
+ ) + } - const postRoute = createRoute({ - getParentRoute: () => postsRoute, - path: '$postId', - component: PostComponent, - validateSearch: z.object({ - _test: z.boolean().optional(), - }), - }) + const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '$postId', + component: PostComponent, + validateSearch: z.object({ + _test: z.boolean().optional(), + }), + }) - const detailRoute = createRoute({ - getParentRoute: () => postRoute, - path: 'detail/', - component: () => , - }) + const detailRoute = createRoute({ + getParentRoute: () => postRoute, + path: 'detail', + component: () => , + }) - const router = createRouter({ - routeTree: rootRoute.addChildren([ - indexRoute, - postsRoute.addChildren([postRoute.addChildren([detailRoute])]), - ]), - }) + const router = createRouter({ + routeTree: rootRoute.addChildren([ + indexRoute, + postsRoute.addChildren([ + postRoute.addChildren([detailRoute]) + ]) + ]), + trailingSlash: trailingSlash ? 'always' : 'never' + }) - render() + render() - const postsButton = await screen.findByTestId('posts-btn') + const postsButton = await screen.findByTestId('posts-btn') - fireEvent.click(postsButton) + fireEvent.click(postsButton) - expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument() + expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument() - const post1Button = await screen.findByTestId('first-post-btn') + const post1Button = await screen.findByTestId('first-post-btn') - fireEvent.click(post1Button) - expect(await screen.findByTestId('post-heading')).toBeInTheDocument() - expect(await screen.findByTestId('detail-heading-1')).toBeInTheDocument() - expect(await screen.findByTestId('detail-heading-2')).toBeInTheDocument() - expect(await screen.findByTestId('detail-heading-1')).toHaveTextContent( - 'Post Path "/id1/detail-1', - ) - expect(await screen.findByTestId('detail-heading-2')).toHaveTextContent( - 'Post Path "/id1/detail-2', - ) + fireEvent.click(post1Button) + expect(await screen.findByTestId('post-heading')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-1')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-2')).toBeInTheDocument() + expect(await screen.findByTestId('detail-heading-1')).toHaveTextContent('Post Path "/id1/detail-1') + expect(await screen.findByTestId('detail-heading-2')).toHaveTextContent('Post Path "/id1/detail-2') - const detail1AddBtn = await screen.findByTestId('detail-btn-add-1') + const detail1AddBtn = await screen.findByTestId('detail-btn-add-1') - fireEvent.click(detail1AddBtn) + fireEvent.click(detail1AddBtn) - expect(router.state.location.pathname).toBe('/posts/id1/detail') - expect(router.state.location.search).toEqual({ _test: true }) + expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`) + expect(router.state.location.search).toEqual({ _test: true }) - const detail1RemoveBtn = await screen.findByTestId('detail-btn-remove-1') + const detail1RemoveBtn = await screen.findByTestId('detail-btn-remove-1') - fireEvent.click(detail1RemoveBtn) + fireEvent.click(detail1RemoveBtn) - expect(router.state.location.pathname).toBe('/posts/id1/detail') - expect(router.state.location.search).toEqual({}) + expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`) + expect(router.state.location.search).toEqual({ }) - const detail2AddBtn = await screen.findByTestId('detail-btn-add-2') + const detail2AddBtn = await screen.findByTestId('detail-btn-add-2') - fireEvent.click(detail2AddBtn) + fireEvent.click(detail2AddBtn) - expect(router.state.location.pathname).toBe('/posts/id1/detail') - expect(router.state.location.search).toEqual({ _test: true }) + expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`) + expect(router.state.location.search).toEqual({ _test: true }) }) describe('when on /posts/$postId and navigating to ../ with default `from` /posts', () => { diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index d3faeb73aed..3db589dd4ca 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1406,6 +1406,10 @@ export class RouterCore< }) } + private comparePaths (path1: string, path2: string) { + return path1.replace(/(.+)\/$/, '$1') === path2.replace(/(.+)\/$/, '$1') + } + buildLocation: BuildLocationFn = (opts) => { const build = ( dest: BuildNextOptions & { @@ -1424,12 +1428,14 @@ export class RouterCore< // First let's find the starting pathname // By default, start with the current location let fromPath = lastMatch.fullPath + const toPath = dest.to + ? this.resolvePathWithBase(fromPath, `${dest.to}`) + : this.resolvePathWithBase(fromPath, '.') const routeIsChanging = !!dest.to && - dest.to !== fromPath && - joinPaths([this.resolvePathWithBase(fromPath, `${dest.to}`), '/']) !== - joinPaths([fromPath, '/']) + !this.comparePaths(dest.to.toString(), fromPath) && + !this.comparePaths(toPath, fromPath) // If the route is changing we need to find the relative fromPath if (dest.unsafeRelative === 'path') { @@ -1437,13 +1443,11 @@ export class RouterCore< } else if (routeIsChanging && dest.from) { fromPath = dest.from const existingFrom = [...allFromMatches].reverse().find((d) => { - return ( - d.fullPath === fromPath || d.fullPath === joinPaths([fromPath, '/']) - ) + return this.comparePaths(d.fullPath, fromPath) }) if (!existingFrom) { - console.warn(`Could not find match for from: ${dest.from}`) + console.warn(`Could not find match for from: ${fromPath}`) } } From 952f5e8ae430e5a93b82348267dfa09502ede20a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 6 Jul 2025 15:48:52 +0000 Subject: [PATCH 8/8] ci: apply automated fixes --- .../react-router/tests/useNavigate.test.tsx | 298 ++++++++++-------- packages/router-core/src/router.ts | 2 +- 2 files changed, 165 insertions(+), 135 deletions(-) diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx index 0260c6bfb26..8d98abdf286 100644 --- a/packages/react-router/tests/useNavigate.test.tsx +++ b/packages/react-router/tests/useNavigate.test.tsx @@ -1366,8 +1366,10 @@ test(' navigates only once in ', async () => { expect(navigateSpy.mock.calls.length).toBe(1) }) -test.each([true,false])('should navigate to current route with search params when using "." in nested route structure from Index Route', async (trailingSlash: boolean) => { - const tail = trailingSlash ? '/' : ''; +test.each([true, false])( + 'should navigate to current route with search params when using "." in nested route structure from Index Route', + async (trailingSlash: boolean) => { + const tail = trailingSlash ? '/' : '' const rootRoute = createRootRoute() @@ -1458,135 +1460,141 @@ test.each([true,false])('should navigate to current route with search params whe expect(router.state.location.pathname).toBe(`/post${tail}`) expect(router.state.location.search).toEqual({ param1: 'value2' }) - }) + }, +) -test.each([true,false])('should navigate to current route with changing path params when using "." in nested route structure', async (trailingSlash) => { - const tail = trailingSlash ? '/' : ''; - const rootRoute = createRootRoute() +test.each([true, false])( + 'should navigate to current route with changing path params when using "." in nested route structure', + async (trailingSlash) => { + const tail = trailingSlash ? '/' : '' + const rootRoute = createRootRoute() - const IndexComponent = () => { - const navigate = useNavigate() - return ( - <> -

Index

- - - ) - } + const IndexComponent = () => { + const navigate = useNavigate() + return ( + <> +

Index

+ + + ) + } - const indexRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/', - component: IndexComponent, - }) + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, + }) - const layoutRoute = createRoute({ - getParentRoute: () => rootRoute, - id: '_layout', - component: () => { + const layoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: '_layout', + component: () => { + return ( + <> +

Layout

+ + + ) + }, + }) + + const PostsComponent = () => { + const navigate = postsRoute.useNavigate() return ( <> -

Layout

+

Posts

+ + ) - }, - }) - - const PostsComponent = () => { - const navigate = postsRoute.useNavigate() - return ( - <> -

Posts

- - - - - ) - } + } - const postsRoute = createRoute({ - getParentRoute: () => layoutRoute, - path: 'posts', - component: PostsComponent, - }) + const postsRoute = createRoute({ + getParentRoute: () => layoutRoute, + path: 'posts', + component: PostsComponent, + }) - const PostComponent = () => { - const params = useParams({ strict: false }) - return ( - <> - - Params: {params.postId} - - - ) - } + const PostComponent = () => { + const params = useParams({ strict: false }) + return ( + <> + + Params: {params.postId} + + + ) + } - const postRoute = createRoute({ - getParentRoute: () => postsRoute, - path: '$postId', - component: PostComponent, - }) + const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '$postId', + component: PostComponent, + }) - const router = createRouter({ - routeTree: rootRoute.addChildren([ - indexRoute, - layoutRoute.addChildren([postsRoute.addChildren([postRoute])]), - ]), - trailingSlash: trailingSlash ? 'always' : 'never', - }) + const router = createRouter({ + routeTree: rootRoute.addChildren([ + indexRoute, + layoutRoute.addChildren([postsRoute.addChildren([postRoute])]), + ]), + trailingSlash: trailingSlash ? 'always' : 'never', + }) - render() + render() - const postsButton = await screen.findByTestId('posts-btn') + const postsButton = await screen.findByTestId('posts-btn') - fireEvent.click(postsButton) + fireEvent.click(postsButton) - expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument() - expect(window.location.pathname).toEqual(`/posts${tail}`) + expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument() + expect(window.location.pathname).toEqual(`/posts${tail}`) - const firstPostButton = await screen.findByTestId('first-post-btn') + const firstPostButton = await screen.findByTestId('first-post-btn') - fireEvent.click(firstPostButton) + fireEvent.click(firstPostButton) - expect(await screen.findByTestId('post-id1')).toBeInTheDocument() - expect(window.location.pathname).toEqual(`/posts/id1${tail}`) + expect(await screen.findByTestId('post-id1')).toBeInTheDocument() + expect(window.location.pathname).toEqual(`/posts/id1${tail}`) - const secondPostButton = await screen.findByTestId('second-post-btn') + const secondPostButton = await screen.findByTestId('second-post-btn') - fireEvent.click(secondPostButton) + fireEvent.click(secondPostButton) - expect(await screen.findByTestId('post-id2')).toBeInTheDocument() - expect(window.location.pathname).toEqual(`/posts/id2${tail}`) -}) + expect(await screen.findByTestId('post-id2')).toBeInTheDocument() + expect(window.location.pathname).toEqual(`/posts/id2${tail}`) + }, +) -test.each([true,false])('should navigate to current route with search params when using "." in nested route structure from non-Index Route', async (trailingSlash) => { - const tail = trailingSlash ? '/' : ''; +test.each([true, false])( + 'should navigate to current route with search params when using "." in nested route structure from non-Index Route', + async (trailingSlash) => { + const tail = trailingSlash ? '/' : '' const rootRoute = createRootRoute() const IndexComponent = () => { @@ -1594,7 +1602,10 @@ test.each([true,false])('should navigate to current route with search params whe return ( <>

Index

- @@ -1637,41 +1648,57 @@ test.each([true,false])('should navigate to current route with search params whe const useModal = (name: string) => { const currentOpen = postRoute.useSearch({ select: (search) => search[`_${name}`], - }); + }) - const navigate = useNavigate(); + const navigate = useNavigate() const setModal = React.useCallback( (open: boolean) => { navigate({ - to: ".", - search: (prev: { }) => ({ + to: '.', + search: (prev: {}) => ({ ...prev, [`_${name}`]: open ? true : undefined, }), resetScroll: false, - }); + }) }, [name, navigate], - ); + ) - return [currentOpen, setModal] as const; + return [currentOpen, setModal] as const } - function DetailComponent(props: {id: string}) { - const params = useParams({strict: false}) - const [currentTest, setTest] = useModal("test") + function DetailComponent(props: { id: string }) { + const params = useParams({ strict: false }) + const [currentTest, setTest] = useModal('test') - return <> -
Post Path "/{params.postId}/detail-{props.id}"!
- {currentTest - ? - : } - + return ( + <> +
+ Post Path "/{params.postId}/detail-{props.id}"! +
+ {currentTest ? ( + + ) : ( + + )} + + ) } const PostComponent = () => { - const params = useParams({strict: false}) + const params = useParams({ strict: false }) return (
@@ -1694,17 +1721,15 @@ test.each([true,false])('should navigate to current route with search params whe const detailRoute = createRoute({ getParentRoute: () => postRoute, path: 'detail', - component: () => , + component: () => , }) const router = createRouter({ routeTree: rootRoute.addChildren([ indexRoute, - postsRoute.addChildren([ - postRoute.addChildren([detailRoute]) - ]) + postsRoute.addChildren([postRoute.addChildren([detailRoute])]), ]), - trailingSlash: trailingSlash ? 'always' : 'never' + trailingSlash: trailingSlash ? 'always' : 'never', }) render() @@ -1721,8 +1746,12 @@ test.each([true,false])('should navigate to current route with search params whe expect(await screen.findByTestId('post-heading')).toBeInTheDocument() expect(await screen.findByTestId('detail-heading-1')).toBeInTheDocument() expect(await screen.findByTestId('detail-heading-2')).toBeInTheDocument() - expect(await screen.findByTestId('detail-heading-1')).toHaveTextContent('Post Path "/id1/detail-1') - expect(await screen.findByTestId('detail-heading-2')).toHaveTextContent('Post Path "/id1/detail-2') + expect(await screen.findByTestId('detail-heading-1')).toHaveTextContent( + 'Post Path "/id1/detail-1', + ) + expect(await screen.findByTestId('detail-heading-2')).toHaveTextContent( + 'Post Path "/id1/detail-2', + ) const detail1AddBtn = await screen.findByTestId('detail-btn-add-1') @@ -1736,7 +1765,7 @@ test.each([true,false])('should navigate to current route with search params whe fireEvent.click(detail1RemoveBtn) expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`) - expect(router.state.location.search).toEqual({ }) + expect(router.state.location.search).toEqual({}) const detail2AddBtn = await screen.findByTestId('detail-btn-add-2') @@ -1744,7 +1773,8 @@ test.each([true,false])('should navigate to current route with search params whe expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`) expect(router.state.location.search).toEqual({ _test: true }) -}) + }, +) describe('when on /posts/$postId and navigating to ../ with default `from` /posts', () => { async function runTest(navigateVia: 'Route' | 'RouteApi') { diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 3db589dd4ca..14f50088c87 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1406,7 +1406,7 @@ export class RouterCore< }) } - private comparePaths (path1: string, path2: string) { + private comparePaths(path1: string, path2: string) { return path1.replace(/(.+)\/$/, '$1') === path2.replace(/(.+)\/$/, '$1') }