diff --git a/packages/tuono-router/src/components/NotFound.spec.tsx b/packages/tuono-router/src/components/NotFound.spec.tsx
new file mode 100644
index 00000000..1f82d23d
--- /dev/null
+++ b/packages/tuono-router/src/components/NotFound.spec.tsx
@@ -0,0 +1,114 @@
+import type { HTMLAttributes, JSX } from 'react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+import { cleanup, render, screen } from '@testing-library/react'
+
+import { Route } from '../route'
+import type { RouteComponent, RouteProps } from '../types'
+
+import { NotFound } from './NotFound'
+
+function createRouteComponent(
+ routeType: string,
+ RouteComponentFn: (props: RouteProps) => JSX.Element,
+): RouteComponent {
+ const RootComponent = RouteComponentFn as RouteComponent
+ RootComponent.preload = vi.fn()
+ RootComponent.displayName = routeType
+ return RootComponent
+}
+
+const root = new Route({
+ isRoot: true,
+ component: createRouteComponent('root', ({ children }) => (
+
{children}
+ )),
+})
+
+vi.mock('./Link', () => ({
+ Link: (props: HTMLAttributes): JSX.Element => (
+
+ ),
+}))
+
+vi.mock('../hooks/useServerPayloadData.ts', () => ({
+ useServerPayloadData: (): { data: unknown; isLoading: boolean } => {
+ return {
+ data: undefined,
+ isLoading: false,
+ }
+ },
+}))
+
+const { useRouterContext } = vi.hoisted(() => {
+ return { useRouterContext: vi.fn() }
+})
+
+vi.mock('../components/RouterContext', () => ({
+ useRouterContext,
+}))
+
+describe('', () => {
+ afterEach(cleanup)
+
+ describe('when a custom 404 page exists', () => {
+ it('should render the custom 404 page', () => {
+ useRouterContext.mockReturnValue({
+ router: {
+ routesById: {
+ '/404': new Route({
+ getParentRoute: (): Route => root,
+ component: createRouteComponent('404', () => (
+ custom 404
+ )),
+ }),
+ __root__: root,
+ },
+ },
+ })
+ render()
+ expect(screen.getByTestId('root')).toMatchInlineSnapshot(
+ `
+
+ `,
+ )
+ })
+ })
+
+ describe('when a custom 404 page does not exist', () => {
+ it('should render the default 404 page, wrapped by the root __layout', () => {
+ useRouterContext.mockReturnValue({
+ router: {
+ routesById: {
+ __root__: root,
+ },
+ },
+ })
+ render()
+ expect(screen.getByTestId('root')).toMatchInlineSnapshot(
+ `
+
+ `,
+ )
+ })
+ })
+})
diff --git a/packages/tuono-router/src/components/NotFound.tsx b/packages/tuono-router/src/components/NotFound.tsx
index 2c3a68fa..7ad4e186 100644
--- a/packages/tuono-router/src/components/NotFound.tsx
+++ b/packages/tuono-router/src/components/NotFound.tsx
@@ -1,11 +1,12 @@
import type { JSX } from 'react'
import { useRouterContext } from '../components/RouterContext'
+import { ROOT_ROUTE_ID } from '../route'
import { RouteMatch } from './RouteMatch'
import { Link } from './Link'
-export function NotFound(): JSX.Element {
+export function NotFound(): JSX.Element | null {
const { router } = useRouterContext()
const custom404Route = router.routesById['/404']
@@ -15,10 +16,14 @@ export function NotFound(): JSX.Element {
return
}
+ const RootLayout = router.routesById[ROOT_ROUTE_ID]?.component
+
+ if (!RootLayout) return null
+
return (
- <>
+
404 Not found
Return home
- >
+
)
}
diff --git a/packages/tuono-router/src/route.ts b/packages/tuono-router/src/route.ts
index 33bc7c28..a751c86b 100644
--- a/packages/tuono-router/src/route.ts
+++ b/packages/tuono-router/src/route.ts
@@ -14,7 +14,7 @@ export function createRoute(options: RouteOptions): Route {
return new Route(options)
}
-const rootRouteId = '__root__'
+export const ROOT_ROUTE_ID = '__root__'
export class Route {
options: RouteOptions
@@ -47,10 +47,10 @@ export class Route {
this.parentRoute = this.options.getParentRoute?.()
if (isRoot) {
- this.path = rootRouteId
+ this.path = ROOT_ROUTE_ID
}
- let path: undefined | string = isRoot ? rootRouteId : this.options.path
+ let path: undefined | string = isRoot ? ROOT_ROUTE_ID : this.options.path
// If the path is anything other than an index path, trim it up
if (path && path !== '/') {
@@ -60,17 +60,17 @@ export class Route {
const customId = this.options.id || path
// Strip the parentId prefix from the first level of children
- let id = isRoot ? rootRouteId : joinPaths([customId])
+ let id = isRoot ? ROOT_ROUTE_ID : joinPaths([customId])
- if (path === rootRouteId) {
+ if (path === ROOT_ROUTE_ID) {
path = '/'
}
- if (id !== rootRouteId) {
+ if (id !== ROOT_ROUTE_ID) {
id = joinPaths(['/', id])
}
- const fullPath = id === rootRouteId ? '/' : path
+ const fullPath = id === ROOT_ROUTE_ID ? '/' : path
this.path = path
this.id = id