T
TanStack17mo ago
sensitive-blue

router mock for tests and stories

What's the best way to have a mock implementation of the router? For example: - we have components that we want to render in storybook, but they depend on useParams(). Ideally, we'd just wrap it in a Provider with a given path. - we have cypress component tests, where we mount a component and then interact with it. It might e.g. click a link, but it shouldn't really navigate to that route (because the test is scoped to a component). Ideally, we'd set the navigate method of the router to a spy. In nextJs, we had next-router-mock, a 3rd party package that would mock the router via a Provider. It worked okay (but not great). Is there anything for the TanStack Router? Thanks 🙏
3 Replies
automatic-azure
automatic-azure17mo ago
Right now, I think the easiest thing is to create a minimal router with the route you need to render. For jest unit tests, I have a renderWithTanStack helper function to wrap the component under test. I pass in initialEntries to set the current url in createMemoryHistory and a route id. I parse out the route id and make routes using code based routes.
import {
AnyRoute,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '@tanstack/react-router';
import { render } from '@testing-library/react';
import { ReactNode } from 'react';

type Options = {
initialEntries?: string[];
childRoutePaths?: string[];
};

const createChildRoute = (
parent: AnyRoute,
currentRoute: string,
childRoutes: string[]
) => {
const isLayout = /^_/.test(currentRoute);
let route;

if (isLayout) {
route = createRoute({
getParentRoute: () => parent,
id: currentRoute,
});
} else {
route = createRoute({
getParentRoute: () => parent,
path: currentRoute,
});
}

const childRoute = childRoutes[0];
const newChildRoutes = childRoutes.slice(1);

if (newChildRoutes.length > 0) {
route.addChildren([createChildRoute(route, childRoute, newChildRoutes)]);
}

return route;
};

export const renderWithTanStack = async (
children: ReactNode,
options?: Options
) => {
const { initialEntries, childRoutePaths } = options ?? {};

const root = createRootRoute({
component: () => <div>{children}</div>,
});

if (childRoutePaths) {
const routes = childRoutePaths.map((childRoutePath) => {
const routes = childRoutePath.split('/').filter((path) => path !== '');
const currentRoute = routes[0];
const childRoutes = routes.slice(1);
return createChildRoute(root, currentRoute, childRoutes);
});

root.addChildren(routes);
}

const router = createRouter({
routeTree: root,
history: createMemoryHistory({
initialEntries: initialEntries ?? ['/'],
}),
});

await router.load();

return render(<RouterProvider router={router} />);
};
import {
AnyRoute,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '@tanstack/react-router';
import { render } from '@testing-library/react';
import { ReactNode } from 'react';

type Options = {
initialEntries?: string[];
childRoutePaths?: string[];
};

const createChildRoute = (
parent: AnyRoute,
currentRoute: string,
childRoutes: string[]
) => {
const isLayout = /^_/.test(currentRoute);
let route;

if (isLayout) {
route = createRoute({
getParentRoute: () => parent,
id: currentRoute,
});
} else {
route = createRoute({
getParentRoute: () => parent,
path: currentRoute,
});
}

const childRoute = childRoutes[0];
const newChildRoutes = childRoutes.slice(1);

if (newChildRoutes.length > 0) {
route.addChildren([createChildRoute(route, childRoute, newChildRoutes)]);
}

return route;
};

export const renderWithTanStack = async (
children: ReactNode,
options?: Options
) => {
const { initialEntries, childRoutePaths } = options ?? {};

const root = createRootRoute({
component: () => <div>{children}</div>,
});

if (childRoutePaths) {
const routes = childRoutePaths.map((childRoutePath) => {
const routes = childRoutePath.split('/').filter((path) => path !== '');
const currentRoute = routes[0];
const childRoutes = routes.slice(1);
return createChildRoute(root, currentRoute, childRoutes);
});

root.addChildren(routes);
}

const router = createRouter({
routeTree: root,
history: createMemoryHistory({
initialEntries: initialEntries ?? ['/'],
}),
});

await router.load();

return render(<RouterProvider router={router} />);
};
That code is still a work in progress. Right now you can theoritically pass in an array of route ids, but it won't work without some dedup logic to prevent routes with the same id/path being created. You can see in the tests in the repo that they are also just creating routes using code based routing as well.
sensitive-blue
sensitive-blueOP17mo ago
oh that looks nice, thank you
conscious-sapphire
conscious-sapphire8mo ago
Small necro, but just wanted to say super thanks for this. It plagued us a lot in our team with the testing in vitetest.

Did you find this page helpful?