Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion e2e/solid-start/basic/tests/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ test('client side navigating to a route with scripts', async ({ page }) => {
await page.waitForURL('/')
await page.getByRole('link', { name: 'Scripts', exact: true }).click()
await expect(page.getByTestId('scripts-test-heading')).toBeInViewport()
expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
await page.waitForFunction(() => (window as any).SCRIPT_1 === true)
expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined)
})

Expand Down
47 changes: 45 additions & 2 deletions packages/react-router/tests/hydration-terminal-lane.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanup, render, screen } from '@testing-library/react'
import { act, cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, test, vi } from 'vitest'
import { hydrate } from '@tanstack/router-core/ssr/client'
import { dehydrateSsrMatchId } from '../../router-core/src/ssr/ssr-match-id'
Expand All @@ -20,18 +20,20 @@ function bootstrap(
ssr: AnyRouteMatch['ssr']
data?: unknown
error?: unknown
notFound?: boolean
}>,
): void {
window.$_TSR = {
router: {
manifest: undefined,
matches: matches.map(({ match, status, ssr, data, error }) => ({
matches: matches.map(({ match, status, ssr, data, error, notFound }) => ({
i: dehydrateSsrMatchId(match.id),
l: data,
e: error,
s: status,
ssr,
u: Date.now(),
...(notFound ? { g: true } : {}),
})),
},
h: vi.fn(),
Expand All @@ -44,6 +46,7 @@ function bootstrap(

afterEach(() => {
cleanup()
vi.useRealTimers()
delete window.$_TSR
})

Expand Down Expand Up @@ -95,4 +98,44 @@ describe('hydration terminal lane', () => {
expect(parentLoader).not.toHaveBeenCalled()
expect(childLoader).toHaveBeenCalledTimes(1)
})

test('keeps a hydrated pending fallback through its minimum before a terminal result', async () => {
const rootRoute = createRootRoute({
pendingMs: 0,
pendingMinMs: 100,
pendingComponent: () => <div role="status">Missing page pending</div>,
notFoundComponent: () => <div>Missing page</div>,
})
const router = createRouter({
history: createMemoryHistory({ initialEntries: ['/missing'] }),
routeTree: rootRoute,
})
const matches = router.matchRoutes(router.state.location)
expect(matches[0]?._notFound).toBe(true)
bootstrap([
{
match: matches[0]!,
status: 'pending',
ssr: false,
notFound: true,
},
])

await hydrate(router)
vi.useFakeTimers()
vi.setSystemTime(0)
render(<RouterProvider router={router} />)
expect(screen.getByRole('status')).toHaveTextContent('Missing page pending')

await act(async () => {
await vi.advanceTimersByTimeAsync(99)
})
expect(screen.getByRole('status')).toHaveTextContent('Missing page pending')
expect(screen.queryByText('Missing page')).not.toBeInTheDocument()

await act(async () => {
await vi.advanceTimersByTimeAsync(5)
})
expect(screen.getByText('Missing page')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
createRouter,
} from '../src'

afterEach(cleanup)
afterEach(() => {
cleanup()
vi.useRealTimers()
})

// https://github.com/TanStack/router/issues/4467
test('default pending component renders while lazy route options load', async () => {
Expand Down Expand Up @@ -142,3 +145,78 @@ test('a lazy pending component is offered while the eager loader is still pendin

expect(screen.getByRole('heading', { name: 'Page' })).toBeInTheDocument()
})

test('a lazy pending component does not restart an acknowledged minimum', async () => {
const loader = createControlledPromise<void>()
const lazyPageOptions = createLazyRoute('/page')({
pendingComponent: () => <p role="status">Loading lazy page</p>,
component: () => <h1>Page</h1>,
})
const lazyOptions = createControlledPromise<typeof lazyPageOptions>()
const rootRoute = createRootRoute({ component: Outlet })
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <h1>Index page</h1>,
})
const pageRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/page',
loader: () => loader,
}).lazy(() => lazyOptions)
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, pageRoute]),
history: createMemoryHistory({ initialEntries: ['/'] }),
defaultPendingMs: 0,
defaultPendingMinMs: 100,
defaultPendingComponent: () => <p role="status">Loading default</p>,
})

render(<RouterProvider router={router} />)
expect(
await screen.findByRole('heading', { name: 'Index page' }),
).toBeInTheDocument()
vi.useFakeTimers()
vi.setSystemTime(0)

const navigation = router.navigate({ to: '/page' })
let settled = false
void navigation.then(() => {
settled = true
})
try {
await act(async () => {
await vi.advanceTimersByTimeAsync(0)
})
expect(screen.getByRole('status')).toHaveTextContent('Loading default')

await act(async () => {
await vi.advanceTimersByTimeAsync(25)
lazyOptions.resolve(lazyPageOptions)
loader.resolve()
await vi.advanceTimersByTimeAsync(0)
})
expect(screen.getByRole('status')).toHaveTextContent('Loading lazy page')

await act(async () => {
await vi.advanceTimersByTimeAsync(74)
})
expect(screen.getByRole('status')).toHaveTextContent('Loading lazy page')

await act(async () => {
await vi.advanceTimersByTimeAsync(5)
await Promise.resolve()
})
expect(settled).toBe(true)
await navigation
expect(screen.getByRole('heading', { name: 'Page' })).toBeInTheDocument()
expect(Date.now()).toBeLessThan(125)
} finally {
lazyOptions.resolve(lazyPageOptions)
loader.resolve()
await act(async () => {
await vi.advanceTimersByTimeAsync(1_000)
await navigation
})
}
})
127 changes: 126 additions & 1 deletion packages/react-router/tests/issue-7367-pending-min-redirect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react'
import { cleanup, render, screen } from '@testing-library/react'
import { act, cleanup, render, screen } from '@testing-library/react'
import { createControlledPromise } from '@tanstack/router-core'
import { afterEach, expect, test, vi } from 'vitest'

import {
Expand All @@ -16,6 +17,7 @@ import { sleep } from './utils'
afterEach(() => {
vi.restoreAllMocks()
cleanup()
vi.useRealTimers()
})

// https://github.com/TanStack/router/issues/7367
Expand Down Expand Up @@ -76,3 +78,126 @@ test('immediate pending spinner (pendingMs: 0 + pendingMinMs) with root beforeLo
expect(router.state.location.pathname).toBe('/welcome')
expect(consoleError).not.toHaveBeenCalled()
})

test('a compatible SPA redirect preserves the acknowledged pending minimum', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
const redirectReady = createControlledPromise<void>()
let shouldRedirect = true

const rootRoute = createRootRoute({
component: Outlet,
pendingMs: 0,
pendingMinMs: 100,
pendingComponent: () => <div data-testid="pending">loading</div>,
beforeLoad: async () => {
if (shouldRedirect) {
shouldRedirect = false
await redirectReady
throw redirect({ to: '/welcome', replace: true })
}
},
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div>Index</div>,
})
const welcomeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/welcome',
component: () => <div data-testid="welcome-page">Welcome</div>,
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, welcomeRoute]),
history: createMemoryHistory({ initialEntries: ['/'] }),
})

try {
render(<RouterProvider router={router} />)
await act(async () => {
await vi.advanceTimersByTimeAsync(0)
})
expect(screen.getByTestId('pending')).toBeInTheDocument()

await act(async () => {
await vi.advanceTimersByTimeAsync(25)
redirectReady.resolve()
await vi.advanceTimersByTimeAsync(74)
})
expect(screen.getByTestId('pending')).toBeInTheDocument()
expect(screen.queryByTestId('welcome-page')).not.toBeInTheDocument()

await act(async () => {
await vi.advanceTimersByTimeAsync(5)
})
expect(screen.getByTestId('welcome-page')).toBeInTheDocument()
} finally {
redirectReady.resolve()
await act(async () => {
await vi.advanceTimersByTimeAsync(1_000)
})
vi.useRealTimers()
}
})

test('an incompatible SPA redirect does not inherit the pending minimum', async () => {
const redirectReady = createControlledPromise<void>()
const rootRoute = createRootRoute({ component: Outlet })
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div data-testid="index-page">Index</div>,
})
const sourceRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/source',
pendingMs: 0,
pendingMinMs: 100,
pendingComponent: () => <div data-testid="pending">loading</div>,
beforeLoad: async () => {
await redirectReady
throw redirect({ to: '/welcome', replace: true })
},
})
const welcomeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/welcome',
component: () => <div data-testid="welcome-page">Welcome</div>,
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, sourceRoute, welcomeRoute]),
history: createMemoryHistory({ initialEntries: ['/'] }),
})

render(<RouterProvider router={router} />)
expect(await screen.findByTestId('index-page')).toBeVisible()
vi.useFakeTimers()
vi.setSystemTime(0)

const navigation = router.navigate({ to: '/source' })
try {
await act(async () => {
await vi.advanceTimersByTimeAsync(0)
})
expect(screen.getByTestId('pending')).toBeVisible()

await act(async () => {
await vi.advanceTimersByTimeAsync(25)
redirectReady.resolve()
await vi.advanceTimersByTimeAsync(5)
await navigation
})

expect(Date.now()).toBeLessThan(100)
expect(screen.getByTestId('welcome-page')).toBeVisible()
expect(screen.queryByTestId('index-page')).not.toBeInTheDocument()
expect(screen.queryByTestId('pending')).not.toBeInTheDocument()
} finally {
redirectReady.resolve()
await act(async () => {
await vi.advanceTimersByTimeAsync(1_000)
await navigation
})
}
})
Loading
Loading