diff --git a/e2e/react-start/basic/src/routes/posts.tsx b/e2e/react-start/basic/src/routes/posts.tsx
index 0f69c183419..22fb7e60a85 100644
--- a/e2e/react-start/basic/src/routes/posts.tsx
+++ b/e2e/react-start/basic/src/routes/posts.tsx
@@ -1,3 +1,4 @@
+import { useState } from 'react'
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '~/utils/posts'
@@ -16,9 +17,16 @@ export const Route = createFileRoute('/posts')({
function PostsComponent() {
const posts = Route.useLoaderData()
+ const [hydrationCount, setHydrationCount] = useState(0)
return (
+
@@ -56,9 +65,14 @@ function setup({ failVia }: { failVia: 'render' | 'loader' }) {
},
})
- const childLoader = vi.fn(() => {
+ let childLoaderCalls = 0
+ const childLoader = vi.fn(async () => {
+ childLoaderCalls++
+ if (childLoaderCalls === 2) {
+ await secondChildLoad
+ }
if (failVia === 'loader') {
- throw new Error('test error')
+ throw new Error('loader error')
}
return 'data'
})
@@ -69,7 +83,7 @@ function setup({ failVia }: { failVia: 'render' | 'loader' }) {
loader: childLoader,
component: function ChildComponent() {
if (failVia === 'render') {
- throw new Error('test error')
+ throw new Error('render error')
}
return child content
},
@@ -85,7 +99,14 @@ function setup({ failVia }: { failVia: 'render' | 'loader' }) {
},
})
- return { router, childLoader, getErrorRenders: () => errorRenders }
+ return {
+ router,
+ childLoader,
+ parentAction,
+ secondChildLoad,
+ getErrorRenders: () => errorRenders,
+ getInvalidation: () => invalidation,
+ }
}
test.each(['render', 'loader'] as const)(
@@ -94,37 +115,71 @@ test.each(['render', 'loader'] as const)(
// Error boundaries log caught errors through console.error, and so does a
// hooks-order crash. Capture instead of polluting the test output, then
// inspect the captured calls for the crash signature.
+ const {
+ router,
+ childLoader,
+ parentAction,
+ secondChildLoad,
+ getErrorRenders,
+ getInvalidation,
+ } = setup({ failVia })
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
- const { router, childLoader, getErrorRenders } = setup({ failVia })
- render()
-
- expect(await screen.findByTestId('error-ui')).toBeInTheDocument()
- const initialErrorRenders = getErrorRenders()
- const initialLoaderCalls = childLoader.mock.calls.length
-
- fireEvent.click(screen.getByTestId('invalidate'))
-
- // The invalidated reload must actually complete: the loader re-ran ...
- await waitFor(() => {
- expect(childLoader.mock.calls.length).toBeGreaterThan(initialLoaderCalls)
- })
-
- // ... the error UI is rendered again after the reload ...
- await waitFor(() => {
- expect(screen.getByTestId('error-ui')).toBeInTheDocument()
- expect(getErrorRenders()).toBeGreaterThan(initialErrorRenders)
- })
-
- // React must not have torn the tree down with a hooks-order violation.
- const hooksCrash = consoleError.mock.calls.find((call) =>
- call.some((arg) =>
- String(arg?.message ?? arg).includes('Rendered more hooks'),
- ),
- )
- expect(hooksCrash).toBeUndefined()
- // The surrounding route (the issue's "frozen" parent) is still mounted
- // and interactive.
- expect(screen.getByTestId('invalidate')).toBeInTheDocument()
+ try {
+ render()
+
+ expect(await screen.findByTestId('error-ui')).toHaveTextContent(
+ `error: ${failVia} error`,
+ )
+ const initialErrorRenders = getErrorRenders()
+ expect(childLoader).toHaveBeenCalledTimes(1)
+ consoleError.mockClear()
+
+ fireEvent.click(screen.getByTestId('invalidate'))
+
+ await waitFor(() => {
+ expect(childLoader).toHaveBeenCalledTimes(2)
+ expect(screen.getByTestId('invalidate')).toHaveTextContent('pending')
+ expect(screen.getByTestId('invalidate')).toBeDisabled()
+ })
+ expect(secondChildLoad.status).toBe('pending')
+
+ const invalidation = getInvalidation()
+ if (!invalidation) {
+ throw new Error('invalidate action did not return its promise')
+ }
+
+ await act(async () => {
+ secondChildLoad.resolve()
+ await invalidation
+ })
+
+ await waitFor(() => {
+ expect(screen.getByTestId('error-ui')).toHaveTextContent(
+ `error: ${failVia} error`,
+ )
+ expect(getErrorRenders()).toBeGreaterThan(initialErrorRenders)
+ expect(screen.getByTestId('invalidate')).toHaveTextContent('invalidate')
+ expect(screen.getByTestId('invalidate')).toBeEnabled()
+ })
+
+ fireEvent.click(screen.getByTestId('parent-action'))
+ expect(parentAction).toHaveBeenCalledTimes(1)
+
+ const hooksCrash = consoleError.mock.calls.find((call) =>
+ call.some((arg) =>
+ String(arg?.message ?? arg).includes('Rendered more hooks'),
+ ),
+ )
+ expect(hooksCrash).toBeUndefined()
+ } finally {
+ if (secondChildLoad.status === 'pending') {
+ await act(async () => {
+ secondChildLoad.resolve()
+ await getInvalidation()?.catch(() => undefined)
+ })
+ }
+ consoleError.mockRestore()
+ }
},
)
diff --git a/packages/react-router/tests/loaders.test.tsx b/packages/react-router/tests/loaders.test.tsx
index d9b5968d723..869aeec76c3 100644
--- a/packages/react-router/tests/loaders.test.tsx
+++ b/packages/react-router/tests/loaders.test.tsx
@@ -912,30 +912,25 @@ test('reproducer for #6388 - rapid navigation between parameterized routes shoul
})
render()
- await act(() => router.latestLoadPromise)
-
- const pendingComponent = screen.findByTestId('pending-component')
expect(await screen.findByTestId('home-page')).toBeInTheDocument()
-
const param1Link = await screen.findByTestId('link-to-param-1')
fireEvent.click(param1Link)
- expect(await pendingComponent).toBeInTheDocument()
+ expect(await screen.findByTestId('pending-component')).toBeInTheDocument()
const param2Link = await screen.findByTestId('link-to-param-2')
fireEvent.click(param2Link)
- expect(await pendingComponent).toBeInTheDocument()
+ expect(await screen.findByTestId('pending-component')).toBeInTheDocument()
fireEvent.click(param1Link)
- expect(await pendingComponent).toBeInTheDocument()
+ expect(await screen.findByTestId('pending-component')).toBeInTheDocument()
- await act(() => router.latestLoadPromise)
+ const paramPage = await screen.findByTestId('param-page')
expect(onAbortMock).toHaveBeenCalled()
expect(errorComponentRenderCount).not.toHaveBeenCalled()
expect(screen.queryByTestId('error-component')).not.toBeInTheDocument()
- expect(await pendingComponent).not.toBeInTheDocument()
+ expect(screen.queryByTestId('pending-component')).not.toBeInTheDocument()
- const paramPage = await screen.findByTestId('param-page')
expect(paramPage).toBeInTheDocument()
expect(paramPage).toHaveTextContent('Param Component 1 Done')
expect(loaderCompleteMock).toHaveBeenCalled()
diff --git a/packages/router-core/tests/issue-3293-on-enter-after-loader.test.ts b/packages/router-core/tests/issue-3293-on-enter-after-loader.test.ts
new file mode 100644
index 00000000000..a1be9103149
--- /dev/null
+++ b/packages/router-core/tests/issue-3293-on-enter-after-loader.test.ts
@@ -0,0 +1,108 @@
+import { expect, test, vi } from 'vitest'
+import { createMemoryHistory } from '@tanstack/history'
+import { BaseRootRoute, BaseRoute, createControlledPromise } from '../src'
+import { createTestRouter } from './routerTestUtils'
+
+// Existing-behavior controls for https://github.com/TanStack/router/issues/3293
+test('#3293 existing behavior: direct load runs onEnter after beforeLoad and loader', async () => {
+ const beforeLoadGate = createControlledPromise<{ ready: true }>()
+ const loaderGate = createControlledPromise<{ someData: 42 }>()
+ const beforeLoad = vi.fn(() => beforeLoadGate)
+ const loader = vi.fn(() => loaderGate)
+ const onEnter = vi.fn()
+
+ const rootRoute = new BaseRootRoute({})
+ const aboutRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ beforeLoad,
+ loader,
+ onEnter,
+ })
+ const router = createTestRouter({
+ routeTree: rootRoute.addChildren([aboutRoute]),
+ history: createMemoryHistory({ initialEntries: ['/about'] }),
+ })
+
+ const load = router.load()
+ await vi.waitFor(() => expect(beforeLoad).toHaveBeenCalledTimes(1))
+ expect(loader).not.toHaveBeenCalled()
+ expect(onEnter).not.toHaveBeenCalled()
+
+ beforeLoadGate.resolve({ ready: true })
+ await vi.waitFor(() => expect(loader).toHaveBeenCalledTimes(1))
+ expect(loader).toHaveBeenCalledWith(
+ expect.objectContaining({
+ context: expect.objectContaining({ ready: true }),
+ }),
+ )
+ expect(onEnter).not.toHaveBeenCalled()
+
+ loaderGate.resolve({ someData: 42 })
+ await load
+
+ expect(onEnter).toHaveBeenCalledTimes(1)
+ expect(onEnter).toHaveBeenCalledWith(
+ expect.objectContaining({
+ status: 'success',
+ context: expect.objectContaining({ ready: true }),
+ loaderData: { someData: 42 },
+ }),
+ )
+})
+
+test('#3293 existing behavior: uncached SPA navigation runs onEnter after beforeLoad and loader', async () => {
+ const beforeLoadGate = createControlledPromise<{ ready: true }>()
+ const loaderGate = createControlledPromise<{ someData: 42 }>()
+ const beforeLoad = vi.fn(() => beforeLoadGate)
+ const loader = vi.fn(() => loaderGate)
+ const onEnter = vi.fn()
+
+ const rootRoute = new BaseRootRoute({})
+ const indexRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ })
+ const aboutRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ beforeLoad,
+ loader,
+ onEnter,
+ })
+ const router = createTestRouter({
+ routeTree: rootRoute.addChildren([indexRoute, aboutRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ await router.load()
+ expect(beforeLoad).not.toHaveBeenCalled()
+ expect(loader).not.toHaveBeenCalled()
+ expect(onEnter).not.toHaveBeenCalled()
+
+ const navigation = router.navigate({ to: '/about' })
+ await vi.waitFor(() => expect(beforeLoad).toHaveBeenCalledTimes(1))
+ expect(loader).not.toHaveBeenCalled()
+ expect(onEnter).not.toHaveBeenCalled()
+
+ beforeLoadGate.resolve({ ready: true })
+ await vi.waitFor(() => expect(loader).toHaveBeenCalledTimes(1))
+ expect(loader).toHaveBeenCalledWith(
+ expect.objectContaining({
+ context: expect.objectContaining({ ready: true }),
+ }),
+ )
+ expect(onEnter).not.toHaveBeenCalled()
+
+ loaderGate.resolve({ someData: 42 })
+ await navigation
+
+ expect(onEnter).toHaveBeenCalledTimes(1)
+ expect(onEnter).toHaveBeenCalledWith(
+ expect.objectContaining({
+ status: 'success',
+ context: expect.objectContaining({ ready: true }),
+ loaderData: { someData: 42 },
+ }),
+ )
+})
diff --git a/packages/router-core/tests/issue-4078-loader-notfound-root-boundary.test.ts b/packages/router-core/tests/issue-4078-loader-notfound-root-boundary.test.ts
new file mode 100644
index 00000000000..45085a8e1fc
--- /dev/null
+++ b/packages/router-core/tests/issue-4078-loader-notfound-root-boundary.test.ts
@@ -0,0 +1,111 @@
+import { describe, expect, it } from 'vitest'
+import { createMemoryHistory } from '@tanstack/history'
+import {
+ BaseRootRoute,
+ BaseRoute,
+ createControlledPromise,
+ notFound,
+ rootRouteId,
+} from '../src'
+import { createTestRouter } from './routerTestUtils'
+
+// Existing Core attribution coverage related to:
+// https://github.com/TanStack/router/issues/4078
+// https://github.com/TanStack/router/issues/2255
+// Throwing an untargeted notFound() in a child loader used to always render the
+// defaultNotFoundComponent, even when __root__ defined a notFoundComponent
+// (#4078). #2255 is the same asymmetry: a path-mismatch notFound renders the
+// root component + notFoundComponent, but a loader-thrown notFound did not.
+//
+// At Router Core's boundary, root attribution is represented by the root match
+// with globalNotFound. These assertions do not distinguish the
+// configured root notFoundComponent from the router default; that reported
+// rendering behavior requires framework-level coverage.
+describe('#4078 / #2255 existing Core root-boundary attribution', () => {
+ const setup = (initialEntries: Array) => {
+ const loaderStarted = createControlledPromise()
+ const loaderResponse = createControlledPromise()
+ const rootRoute = new BaseRootRoute({
+ component: () => 'Root',
+ notFoundComponent: () => 'Root not found',
+ })
+ const indexRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ })
+ const aboutRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ loader: async () => {
+ loaderStarted.resolve()
+ await loaderResponse
+ throw notFound()
+ },
+ component: () => 'About',
+ })
+
+ return {
+ router: createTestRouter({
+ routeTree: rootRoute.addChildren([indexRoute, aboutRoute]),
+ history: createMemoryHistory({ initialEntries }),
+ }),
+ loaderStarted,
+ loaderResponse,
+ }
+ }
+
+ const getRootBoundaryProjection = (
+ router: ReturnType['router'],
+ ) => {
+ const rootMatch = router.state.matches.find(
+ (match) => match.routeId === rootRouteId,
+ )
+
+ return {
+ routeId: rootMatch?.routeId,
+ globalNotFound: rootMatch?.globalNotFound,
+ }
+ }
+
+ it('an async loader notFound navigation selects the root boundary', async () => {
+ const { router, loaderStarted, loaderResponse } = setup(['/'])
+ await router.load()
+
+ const navigation = router.navigate({ to: '/about' })
+ await loaderStarted
+ try {
+ expect(loaderResponse.status).toBe('pending')
+ } finally {
+ loaderResponse.resolve()
+ }
+
+ await navigation
+
+ expect(router.state.location.pathname).toBe('/about')
+ expect(getRootBoundaryProjection(router)).toEqual({
+ routeId: rootRouteId,
+ globalNotFound: true,
+ })
+ })
+
+ it('matches the root-boundary projection of an unmatched URL (#2255 parity)', async () => {
+ const unmatched = setup(['/missing'])
+ await unmatched.router.load()
+ const unmatchedProjection = getRootBoundaryProjection(unmatched.router)
+ expect(unmatchedProjection).toEqual({
+ routeId: rootRouteId,
+ globalNotFound: true,
+ })
+
+ const loaderNotFound = setup(['/'])
+ await loaderNotFound.router.load()
+ const navigation = loaderNotFound.router.navigate({ to: '/about' })
+ await loaderNotFound.loaderStarted
+ loaderNotFound.loaderResponse.resolve()
+ await navigation
+
+ expect(getRootBoundaryProjection(loaderNotFound.router)).toEqual(
+ unmatchedProjection,
+ )
+ })
+})
diff --git a/packages/router-core/tests/issue-4696-parent-context-search-normalization.test.ts b/packages/router-core/tests/issue-4696-parent-context-search-normalization.test.ts
new file mode 100644
index 00000000000..20ef368318f
--- /dev/null
+++ b/packages/router-core/tests/issue-4696-parent-context-search-normalization.test.ts
@@ -0,0 +1,102 @@
+import { expect, test, vi } from 'vitest'
+import { createMemoryHistory } from '@tanstack/history'
+import { BaseRootRoute, BaseRoute } from '../src'
+import { createTestRouter } from './routerTestUtils'
+
+// Existing-behavior coverage for https://github.com/TanStack/router/issues/4696
+test('#4696 existing behavior: normalized child search keeps reused parent context', async () => {
+ const rootLoader = vi.fn(
+ ({ context }: { context: Record }) => context,
+ )
+ const dashboardBeforeLoad = vi.fn(
+ ({ context }: { context: Record }) => {
+ if (!context.isAuthenticated) {
+ throw new Error('Authentication context was lost')
+ }
+ },
+ )
+ const rootRoute = new BaseRootRoute({
+ beforeLoad: async () => {
+ await Promise.resolve()
+ return {
+ initialData: {
+ user: { email: 'mr.user@gmail.com', role: 'user' },
+ },
+ initializationError: undefined,
+ isAuthenticated: true,
+ isAdmin: false,
+ }
+ },
+ loader: rootLoader,
+ })
+ const indexRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ })
+ const dashboardRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/dashboard',
+ validateSearch: () => ({ page: 0 }),
+ beforeLoad: dashboardBeforeLoad,
+ })
+ const history = createMemoryHistory({ initialEntries: ['/'] })
+ const router = createTestRouter({
+ routeTree: rootRoute.addChildren([indexRoute, dashboardRoute]),
+ history,
+ })
+
+ await router.load()
+ expect(rootLoader).toHaveBeenCalledTimes(1)
+ expect(rootLoader.mock.calls[0]?.[0].context).toMatchObject({
+ isAuthenticated: true,
+ })
+ expect(router.state.location.pathname).toBe('/')
+ router.update({
+ history: createMemoryHistory({
+ initialEntries: ['/dashboard?page=0'],
+ }),
+ })
+ await router.load()
+ expect(router.state.location.pathname).toBe('/dashboard')
+ expect(rootLoader).toHaveBeenCalledTimes(1)
+ expect(dashboardBeforeLoad).toHaveBeenCalledTimes(1)
+ expect(dashboardBeforeLoad).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ context: expect.objectContaining({ isAuthenticated: true }),
+ }),
+ )
+ expect(
+ router.state.matches.find((match) => match.routeId === rootRoute.id)
+ ?.loaderData,
+ ).toMatchObject({ isAuthenticated: true })
+
+ dashboardBeforeLoad.mockClear()
+ router.update({
+ history: createMemoryHistory({ initialEntries: ['/dashboard'] }),
+ })
+ await router.load()
+
+ expect(dashboardBeforeLoad).toHaveBeenCalledTimes(1)
+ expect(dashboardBeforeLoad).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ context: expect.objectContaining({ isAuthenticated: true }),
+ }),
+ )
+ expect(rootLoader).toHaveBeenCalledTimes(1)
+ expect(router.state.location.pathname).toBe('/dashboard')
+ const rootMatch = router.state.matches.find(
+ (match) => match.routeId === rootRoute.id,
+ )
+ const dashboardMatch = router.state.matches.find(
+ (match) => match.routeId === dashboardRoute.id,
+ )
+ expect(rootMatch?.loaderData).toMatchObject({
+ isAuthenticated: true,
+ isAdmin: false,
+ })
+ expect(dashboardMatch).toMatchObject({
+ status: 'success',
+ search: { page: 0 },
+ searchError: undefined,
+ })
+})
diff --git a/packages/router-core/tests/issue-5106-hydrated-notfound-boundary.test.ts b/packages/router-core/tests/issue-5106-hydrated-notfound-boundary.test.ts
new file mode 100644
index 00000000000..8a1570a3f41
--- /dev/null
+++ b/packages/router-core/tests/issue-5106-hydrated-notfound-boundary.test.ts
@@ -0,0 +1,151 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { createMemoryHistory } from '@tanstack/history'
+import { BaseRootRoute, BaseRoute, isNotFound } from '../src'
+import { hydrate } from '../src/ssr/client'
+import { createTestRouter } from './routerTestUtils'
+import type { TsrSsrGlobal } from '../src/ssr/types'
+import type { Manifest } from '../src/manifest'
+
+const testManifest: Manifest = { routes: {} }
+
+// Supplemental Core state coverage for
+// https://github.com/TanStack/router/issues/5106. The reported rendered React
+// hydration symptom is covered by the React Start E2E test.
+describe('hydrated child-owned notFound boundary coverage', () => {
+ let mockWindow: { $_TSR?: TsrSsrGlobal }
+
+ beforeEach(() => {
+ mockWindow = {}
+ ;(global as any).window = mockWindow
+ })
+
+ afterEach(() => {
+ delete (global as any).window
+ vi.restoreAllMocks()
+ })
+
+ it('#5106 existing behavior: adopts a child-owned boundary without invoking its loaders during hydration', async () => {
+ const postsLoader = vi.fn(() => 'posts-data')
+ const postLoader = vi.fn(() => 'post-data')
+ const history = createMemoryHistory({
+ initialEntries: ['/posts/i-do-not-exist'],
+ })
+
+ const rootRoute = new BaseRootRoute({})
+ const postsRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/posts',
+ loader: postsLoader,
+ component: () => 'Posts',
+ })
+ const postRoute = new BaseRoute({
+ getParentRoute: () => postsRoute,
+ path: '/$postId',
+ loader: postLoader,
+ component: () => 'Post',
+ notFoundComponent: () => 'Post not found',
+ })
+ const safeLoader = vi.fn(() => 'safe-data')
+ const safeRoute = new BaseRoute({
+ getParentRoute: () => rootRoute,
+ path: '/safe',
+ loader: safeLoader,
+ })
+
+ const router = createTestRouter({
+ routeTree: rootRoute.addChildren([
+ postsRoute.addChildren([postRoute]),
+ safeRoute,
+ ]),
+ history,
+ isServer: false,
+ })
+
+ const matches = router.matchRoutes(router.stores.location.get())
+ expect(matches.map((match) => match.routeId)).toEqual([
+ rootRoute.id,
+ postsRoute.id,
+ postRoute.id,
+ ])
+
+ mockWindow.$_TSR = {
+ router: {
+ manifest: testManifest,
+ dehydratedData: {},
+ // This synthetic payload models a child-owned boundary, so its
+ // terminal prefix includes the child match.
+ matches: [
+ {
+ i: matches[0]!.id,
+ s: 'success' as const,
+ ssr: true,
+ u: Date.now(),
+ },
+ {
+ i: matches[1]!.id,
+ s: 'success' as const,
+ l: 'posts-data',
+ ssr: true,
+ u: Date.now(),
+ },
+ {
+ i: matches[2]!.id,
+ s: 'notFound' as const,
+ e: { isNotFound: true },
+ ssr: true,
+ u: Date.now(),
+ },
+ ],
+ },
+ h: vi.fn(),
+ e: vi.fn(),
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ }
+
+ await hydrate(router)
+
+ const stateMatches = router.state.matches
+ expect(router.state.location.pathname).toBe('/posts/i-do-not-exist')
+ expect(router.state.isLoading).toBe(false)
+ expect(stateMatches.map((match) => match.routeId)).toEqual([
+ rootRoute.id,
+ postsRoute.id,
+ postRoute.id,
+ ])
+
+ // The parent keeps the data and status supplied by the payload.
+ expect(stateMatches[1]!.status).toBe('success')
+ expect(stateMatches[1]!.loaderData).toBe('posts-data')
+
+ // The child is adopted as the notFound boundary.
+ expect(stateMatches[2]!.status).toBe('notFound')
+ expect(isNotFound(stateMatches[2]!.error)).toBe(true)
+
+ // Hydration does not invoke either route loader represented by the
+ // payload.
+ expect(postsLoader).not.toHaveBeenCalled()
+ expect(postLoader).not.toHaveBeenCalled()
+ expect(safeLoader).not.toHaveBeenCalled()
+
+ // Supplemental public liveness coverage: a later client navigation still
+ // completes. This does not exercise React's rendered hydration boundary.
+ await router.navigate({ to: '/safe' })
+ expect(router.state.location.pathname).toBe('/safe')
+ expect(router.state.isLoading).toBe(false)
+ expect(router.state.matches.map((match) => match.routeId)).toEqual([
+ rootRoute.id,
+ safeRoute.id,
+ ])
+ expect(router.state.matches.at(-1)).toMatchObject({
+ routeId: safeRoute.id,
+ status: 'success',
+ loaderData: 'safe-data',
+ })
+ expect(safeLoader).toHaveBeenCalledTimes(1)
+ expect(postsLoader).not.toHaveBeenCalled()
+ expect(postLoader).not.toHaveBeenCalled()
+ })
+})