Testing
λ Router provides createMemoryNavigation() for testing and SSR environments where the browser’s Navigation API is not available. This creates a minimal in-memory Navigation object that satisfies the router’s requirements.
createMemoryNavigation
Section titled “createMemoryNavigation”import { createMemoryNavigation } from "@studiolambda/router/react";
const navigation = createMemoryNavigation({ url: "https://example.com/user/42?tab=posts",});Options
Section titled “Options”| Option | Type | Description |
|---|---|---|
url | string | The initial URL. Must be a full URL with protocol and host. |
Behavior
Section titled “Behavior”The memory navigation provides:
currentEntry.url— returns the initial URL.addEventListener/removeEventListener— no-ops.navigate()— no-op returning a pre-resolvedNavigationResult.canGoBack/canGoForward— alwaysfalse.entries()— single-entry array.transition—null.
This is sufficient for rendering the Router in a test environment where you want to verify what renders for a given URL without actual navigation.
Testing a Route Component
Section titled “Testing a Route Component”Use the Router with a memory navigation to render a specific route:
import { describe, it, expect } from "vitest";import { render, screen } from "@testing-library/react";import { Suspense } from "react";import { createRouter, Router, createMemoryNavigation,} from "@studiolambda/router/react";
function UserProfile() { const { id } = useParams(); return <h1>User #{id}</h1>;}
describe("UserProfile", () => { it("renders the user id from params", () => { const router = createRouter(function (route) { route("/user/:id").render(UserProfile); });
const navigation = createMemoryNavigation({ url: "https://example.com/user/42", });
render( <Suspense fallback="loading"> <Router matcher={router} navigation={navigation} /> </Suspense> );
expect(screen.getByText("User #42")).toBeInTheDocument(); });});Testing the Not Found Component
Section titled “Testing the Not Found Component”it("renders not found for unmatched URLs", () => { const router = createRouter(function (route) { route("/").render(Home); });
function Custom404() { return <div>Page not found</div>; }
const navigation = createMemoryNavigation({ url: "https://example.com/nonexistent", });
render( <Suspense fallback="loading"> <Router matcher={router} navigation={navigation} notFound={Custom404} /> </Suspense> );
expect(screen.getByText("Page not found")).toBeInTheDocument();});Testing Middleware
Section titled “Testing Middleware”Test that middleware correctly guards routes:
const AuthContext = createContext<{ user: User | null }>({ user: null });
function AuthGuard({ children }: MiddlewareProps) { const { user } = use(AuthContext); if (!user) return <div>Please log in</div>; return children;}
it("shows login prompt when not authenticated", () => { const router = createRouter(function (route) { route("/dashboard").middleware([AuthGuard]).render(Dashboard); });
const navigation = createMemoryNavigation({ url: "https://example.com/dashboard", });
render( <AuthContext value={{ user: null }}> <Suspense fallback="loading"> <Router matcher={router} navigation={navigation} /> </Suspense> </AuthContext> );
expect(screen.getByText("Please log in")).toBeInTheDocument();});
it("renders dashboard when authenticated", () => { const router = createRouter(function (route) { route("/dashboard").middleware([AuthGuard]).render(Dashboard); });
const navigation = createMemoryNavigation({ url: "https://example.com/dashboard", });
render( <AuthContext value={{ user: { name: "Erik" } }}> <Suspense fallback="loading"> <Router matcher={router} navigation={navigation} /> </Suspense> </AuthContext> );
expect(screen.getByText("Dashboard")).toBeInTheDocument();});Testing Prefetch Handlers
Section titled “Testing Prefetch Handlers”Test that prefetch functions are called with the correct context:
import { createRouter } from "@studiolambda/router/react";
it("calls prefetch with correct params and url", () => { const prefetchSpy = vi.fn();
const router = createRouter(function (route) { route("/post/:slug").prefetch(prefetchSpy).render(PostPage); });
// Match the route directly using the matcher. const match = router.match("/post/hello-world");
expect(match).not.toBeNull(); expect(match!.params).toEqual({ slug: "hello-world" });
// Call the prefetch handler manually to verify its logic. match!.handler.prefetch?.({ params: match!.params, url: new URL("https://example.com/post/hello-world"), controller: { redirect: vi.fn(), addHandler: vi.fn(), } as unknown as NavigationPrecommitController, });
expect(prefetchSpy).toHaveBeenCalledWith( expect.objectContaining({ params: { slug: "hello-world" }, url: expect.any(URL), }) );});Testing the Matcher Directly
Section titled “Testing the Matcher Directly”The route matcher can be tested independently of React:
import { createRouter } from "@studiolambda/router/react";
it("matches static routes", () => { const router = createRouter(function (route) { route("/").render(Home); route("/about").render(About); });
expect(router.match("/")).not.toBeNull(); expect(router.match("/about")).not.toBeNull(); expect(router.match("/nonexistent")).toBeNull();});
it("extracts dynamic params", () => { const router = createRouter(function (route) { route("/user/:id").render(UserProfile); });
const match = router.match("/user/42"); expect(match?.params).toEqual({ id: "42" });});
it("captures wildcard segments", () => { const router = createRouter(function (route) { route("/files/*path").render(FileViewer); });
const match = router.match("/files/docs/readme.md"); expect(match?.params).toEqual({ path: "docs/readme.md" });});