Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions packages/react-router/tests/issue-8128-context.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { act, cleanup, render, screen } from '@testing-library/react'
import { expect, onTestFinished, test, vi } from 'vitest'
import { dehydrateSsrMatchId } from '../../router-core/src/ssr/ssr-match-id'
import {
Outlet,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'
import { hydrate } from '../src/ssr/client'

test('#8128: leaving a hydrated root not-found page preserves beforeLoad context', async () => {
const rootRenders: Array<string | undefined> = []

vi.useFakeTimers()
onTestFinished(async () => {
cleanup()
delete window.$_TSR
delete (window as Window & { $R?: unknown }).$R
vi.useRealTimers()
})

const rootRoute = createRootRoute({
beforeLoad: async () => {
await new Promise((resolve) => setTimeout(resolve, 250))
return { locale: 'en' }
},
component: () => {
const context = rootRoute.useRouteContext()
rootRenders.push(context.locale)

return (
<>
<output data-testid="root-locale">
Locale: {context.locale ?? 'missing'}
</output>
<Outlet />
</>
)
},
notFoundComponent: () => <div>Not found</div>,
})
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
component: () => <div>Dashboard</div>,
})

const router = createRouter({
routeTree: rootRoute.addChildren([dashboardRoute]),
history: createMemoryHistory({
initialEntries: ['/any-not-found-url'],
}),
defaultPendingComponent: () => <div>Loading</div>,
defaultPendingMs: 150,
defaultPendingMinMs: 0,
})
const rootMatch = router.matchRoutes(router.latestLocation)[0]!
// Model the public server bootstrap for a hydrated root-level not-found.
window.$_TSR = {
router: {
manifest: { routes: {} },
dehydratedData: {},
matches: [
{
i: dehydrateSsrMatchId(rootMatch.id),
b: { locale: 'en' },
u: Date.now(),
s: 'success',
ssr: true,
g: true,
},
],
},
h: vi.fn(),
e: vi.fn(),
c: vi.fn(),
p: vi.fn(),
buffer: [],
initialized: false,
}

await hydrate(router)
render(<RouterProvider router={router} />)

expect(router.state.matches[0]).toMatchObject({
status: 'success',
_notFound: true,
context: { locale: 'en' },
})
expect(screen.getByText('Not found')).toBeInTheDocument()

let navigation!: Promise<void>
await act(async () => {
navigation = router.navigate({ to: '/dashboard' })
await vi.advanceTimersByTimeAsync(150)
})

expect(router.state.matches[0]).toMatchObject({
status: 'success',
_notFound: true,
context: { locale: 'en' },
})
expect(screen.getByTestId('root-locale')).toHaveTextContent('Locale: en')
expect(screen.getByText('Not found')).toBeInTheDocument()
expect(screen.queryByText('Loading')).not.toBeInTheDocument()
expect(rootRenders).not.toContain(undefined)

await act(async () => {
await vi.advanceTimersByTimeAsync(100)
await navigation
})
expect(screen.getByText('Dashboard')).toBeInTheDocument()
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { act, cleanup, render, screen } from '@testing-library/react'
import { afterEach, expect, onTestFinished, test } from 'vitest'
import {
Outlet,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'

function createGate() {
let resolve!: () => void
const promise = new Promise<void>((resolvePromise) => {
resolve = resolvePromise
})

return { promise, resolve }
}

afterEach(cleanup)

test('#8128: returning from a layout-owned fuzzy not-found retains only through its owner', async () => {
const beforeLoadStarted = createGate()
const beforeLoadGate = createGate()
onTestFinished(() => beforeLoadGate.resolve())
let gateNextBeforeLoad = false

const rootRoute = createRootRoute({ component: Outlet })
const agentsLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_agents',
component: () => (
<section>
<h1>Agent operations</h1>
<Outlet />
</section>
),
notFoundComponent: () => <p>Agent page not found</p>,
})
const agentsRoute = createRoute({
getParentRoute: () => agentsLayoutRoute,
path: '/agents',
beforeLoad: async () => {
if (gateNextBeforeLoad) {
gateNextBeforeLoad = false
beforeLoadStarted.resolve()
await beforeLoadGate.promise
}
},
pendingMs: 0,
pendingMinMs: 0,
pendingComponent: () => <p role="status">Loading agents</p>,
component: () => <p>Agents directory</p>,
})
const router = createRouter({
routeTree: rootRoute.addChildren([
agentsLayoutRoute.addChildren([agentsRoute]),
]),
history: createMemoryHistory({ initialEntries: ['/agents'] }),
})

let navigation: Promise<void> | undefined
try {
render(<RouterProvider router={router} />)

expect(await screen.findByText('Agents directory')).toBeVisible()
expect(
screen.getByRole('heading', { name: 'Agent operations' }),
).toBeVisible()

await act(async () => {
navigation = router.navigate({ to: '/agents/missing' } as any)
await navigation
})

expect(
screen.getByRole('heading', { name: 'Agent operations' }),
).toBeVisible()
expect(screen.getByText('Agent page not found')).toBeVisible()
expect(screen.queryByText('Agents directory')).not.toBeInTheDocument()

gateNextBeforeLoad = true
await act(async () => {
navigation = router.navigate({ to: '/agents' })
await beforeLoadStarted.promise
await new Promise((resolve) => setTimeout(resolve, 0))
})

expect(
screen.getByRole('heading', { name: 'Agent operations' }),
).toBeVisible()
expect(screen.queryByText('Agent page not found')).not.toBeInTheDocument()
expect(screen.getByRole('status')).toHaveTextContent('Loading agents')

await act(async () => {
beforeLoadGate.resolve()
await navigation
})

expect(screen.getByText('Agents directory')).toBeVisible()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
} finally {
await act(async () => {
await Promise.allSettled(navigation ? [navigation] : [])
})
}
})
7 changes: 4 additions & 3 deletions packages/router-core/src/load-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1308,15 +1308,16 @@ async function executeClientLane(
if (
committed?.id !== match.id ||
committed.status !== 'success' ||
committed._notFound ||
match.preload ||
visible?.id !== match.id ||
visible.status !== 'success' ||
visible._notFound
visible.status !== 'success'
) {
break
}
retainedEnd++
if (committed._notFound || visible._notFound) {
break
}
}
const tasks: Array<LoaderTask> = []
const start = options[6 /* resolvedPrefix */] ?? 0
Expand Down
Loading