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
226 changes: 126 additions & 100 deletions packages/react-router/tests/errorComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Link,
RouterProvider,
createBrowserHistory,
createLazyRoute,
createRootRoute,
createRoute,
createRouter,
Expand Down Expand Up @@ -38,110 +39,135 @@ afterEach(() => {
cleanup()
})

describe.each([{ preload: false }, { preload: 'intent' }] as const)(
'errorComponent is rendered when the preload=$preload',
(options) => {
describe.each([true, false])('with async=%s', (isAsync) => {
const throwableFn = isAsync ? asyncToThrowFn : throwFn

const callers = [
{ caller: 'beforeLoad', testFn: throwableFn },
{ caller: 'loader', testFn: throwableFn },
]

test.each(callers)(
'an Error is thrown on navigate in the route $caller function',
async ({ caller, testFn }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: function Home() {
return (
<div>
<Link to="/about">link to about</Link>
</div>
describe.each([true, false])(
'with lazy errorComponent=%s',
(isUsingLazyError) => {
describe.each([{ preload: false }, { preload: 'intent' }] as const)(
'errorComponent is rendered when the preload=$preload',
(options) => {
describe.each([true, false])('with async=%s', (isAsync) => {
const throwableFn = isAsync ? asyncToThrowFn : throwFn

const callers = [
{ caller: 'beforeLoad', testFn: throwableFn },
{ caller: 'loader', testFn: throwableFn },
]

test.each(callers)(
'an Error is thrown on navigate in the route $caller function',
async ({ caller, testFn }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: function Home() {
return (
<div>
<Link to="/about">link to about</Link>
</div>
)
},
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
beforeLoad: caller === 'beforeLoad' ? testFn : undefined,
loader: caller === 'loader' ? testFn : undefined,
component: function Home() {
return <div>About route content</div>
},
errorComponent: isUsingLazyError ? undefined : MyErrorComponent,
})

if (isUsingLazyError) {
aboutRoute.lazy(() =>
Promise.resolve(
createLazyRoute('/about')({
errorComponent: MyErrorComponent,
}),
),
)
}

const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])

const router = createRouter({
routeTree,
defaultPreload: options.preload,
history,
})

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

const linkToAbout = await screen.findByRole('link', {
name: 'link to about',
})

expect(linkToAbout).toBeInTheDocument()
fireEvent.mouseOver(linkToAbout)
fireEvent.focus(linkToAbout)
fireEvent.click(linkToAbout)

const errorComponent = await screen.findByText(
`Error: error thrown`,
undefined,
{ timeout: 1500 },
)
await expect(
screen.findByText('About route content'),
).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
beforeLoad: caller === 'beforeLoad' ? testFn : undefined,
loader: caller === 'loader' ? testFn : undefined,
component: function Home() {
return <div>About route content</div>
},
errorComponent: MyErrorComponent,
})

const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])

const router = createRouter({
routeTree,
defaultPreload: options.preload,
history,
})

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

const linkToAbout = await screen.findByRole('link', {
name: 'link to about',
})

expect(linkToAbout).toBeInTheDocument()
fireEvent.mouseOver(linkToAbout)
fireEvent.focus(linkToAbout)
fireEvent.click(linkToAbout)

const errorComponent = await screen.findByText(
`Error: error thrown`,
undefined,
{ timeout: 1500 },
)
await expect(
screen.findByText('About route content'),
).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
)

test.each(callers)(
'an Error is thrown on first load in the route $caller function',
async ({ caller, testFn }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
beforeLoad: caller === 'beforeLoad' ? testFn : undefined,
loader: caller === 'loader' ? testFn : undefined,
component: function Home() {
return <div>Index route content</div>
},
errorComponent: MyErrorComponent,
})

const routeTree = rootRoute.addChildren([indexRoute])

const router = createRouter({
routeTree,
defaultPreload: options.preload,
history,
})

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

const errorComponent = await screen.findByText(
`Error: error thrown`,
undefined,
{ timeout: 750 },
test.each(callers)(
'an Error is thrown on first load in the route $caller function',
async ({ caller, testFn }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
beforeLoad: caller === 'beforeLoad' ? testFn : undefined,
loader: caller === 'loader' ? testFn : undefined,
component: function Home() {
return <div>Index route content</div>
},
errorComponent: isUsingLazyError ? undefined : MyErrorComponent,
})

if (isUsingLazyError) {
indexRoute.lazy(() =>
Promise.resolve(
createLazyRoute('/')({
errorComponent: MyErrorComponent,
}),
),
)
}

const routeTree = rootRoute.addChildren([indexRoute])

const router = createRouter({
routeTree,
defaultPreload: options.preload,
history,
})

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

const errorComponent = await screen.findByText(
`Error: error thrown`,
undefined,
{ timeout: 750 },
)
await expect(
screen.findByText('Index route content'),
).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
)
await expect(
screen.findByText('Index route content'),
).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
)
})
})
},
)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ describe("Store doesn't update *too many* times during navigation", () => {
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
expect(updates).toBeGreaterThanOrEqual(6) // WARN: this is flaky, and sometimes (rarely) is 7
expect(updates).toBeLessThanOrEqual(7)
expect(updates).toBeLessThanOrEqual(8)
})

test('not found in beforeLoad', async () => {
Expand All @@ -197,7 +197,7 @@ describe("Store doesn't update *too many* times during navigation", () => {
// This number should be as small as possible to minimize the amount of work
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
expect(updates).toBe(7)
expect(updates).toBe(8)
})

test('hover preload, then navigate, w/ async loaders', async () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/router-core/src/load-matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,13 +468,22 @@ const executeBeforeLoad = (
if (isPromise(beforeLoadContext)) {
pending()
return beforeLoadContext
.catch((err) => {
.catch(async (err) => {
if (!isRedirect(err)) {
await loadRouteChunk(route)
}
handleSerialError(inner, index, err, 'BEFORE_LOAD')
})
.then(updateContext)
}
} catch (err) {
pending()
if (!isRedirect(err)) {
return loadRouteChunk(route).then(() => {
handleSerialError(inner, index, err, 'BEFORE_LOAD')
updateContext(undefined)
})
}
handleSerialError(inner, index, err, 'BEFORE_LOAD')
}

Expand Down
Loading
Loading