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( + ` +
+
+ custom 404 +
+
+ `, + ) + }) + }) + + 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( + ` +
+

+ 404 Not found +

+
+ Return home + +
+ `, + ) + }) + }) +}) 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