Skip to content

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.

import { createMemoryNavigation } from "@studiolambda/router/react";
const navigation = createMemoryNavigation({
url: "https://example.com/user/42?tab=posts",
});
OptionTypeDescription
urlstringThe initial URL. Must be a full URL with protocol and host.

The memory navigation provides:

  • currentEntry.url — returns the initial URL.
  • addEventListener / removeEventListener — no-ops.
  • navigate() — no-op returning a pre-resolved NavigationResult.
  • canGoBack / canGoForward — always false.
  • entries() — single-entry array.
  • transitionnull.

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.

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();
});
});
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();
});

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();
});

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),
})
);
});

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" });
});