Replies: 13 comments 12 replies
-
It would be nice if we are able to test the routing logic without having to rely on ui framework dependencies. I hope there's an example code for this. |
Beta Was this translation helpful? Give feedback.
-
You can just create a router and call methods on it then test the state.
…On Aug 10, 2023 at 2:20 PM -0400, Kevin Do ***@***.***>, wrote:
It would be nice if we are able to test the routing logic without having to rely on libraries like testing library to traverse the dom or have ui framework as part of our tests. I hope that's possible and I wonder what the code would look like.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
The repo has an example of this.
…On Aug 11, 2023 at 3:39 PM -0400, Kacper Arendt ***@***.***>, wrote:
can you provide some example, i don't get it
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you commented.Message ID: ***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
With React it is as simple as: <RouterProvider router={router} defaultComponent={TestingComponent} /> Then in import { createRootRoute, createRouter } from '@tanstack/react-router';
const rootRoute = createRootRoute();
export const router = createRouter({
routeTree: rootRoute
}); |
Beta Was this translation helpful? Give feedback.
-
Documentation on the main site with testing examples would be great. |
Beta Was this translation helpful? Give feedback.
-
Scared this gets forgotten, but some clearer documentation on Tanstack Router + Testing would be greatly appreciated |
Beta Was this translation helpful? Give feedback.
-
I believe tannerlinsley is referring to something like this - router/packages/react-router/tests/loaders.test.tsx Lines 34 to 59 in 0de8af7 Looks quite straightforward to me, will take it for a spin |
Beta Was this translation helpful? Give feedback.
-
I'm also scared that this might get forgotten, having how to testing the docs would be great especially for those that have a large amount of tests seeking to migrate. |
Beta Was this translation helpful? Give feedback.
-
Want to use this router in a big project and the test are necessary. Hope there is some examples for getting a high percent of code coverage |
Beta Was this translation helpful? Give feedback.
-
Here's what I came up with. This is in a brand new project and I'm new to Tanstack Router so we'll have to see how well this scales as the codebase grows. I like the simplicity of it for now though. // from my index.tsx
export const MyRouterProvider = () => {
return <RouterProvider router={router} />;
};
// in my test
const { getByRole } = render(<MyRouterProvider />);
const link = getByRole("link", { name: "Tests" });
expect(link).toBeInTheDocument(); |
Beta Was this translation helpful? Give feedback.
-
Hey all, I ended up suffering through getting a testing setup working for a few days so I wanted to share my solutions here in case anyone else could benefit from them. Our app has a ton of unit tests, and heavily utilizes page-specific context providers as well. So, I needed to come up with reusable testing helpers that were capable of wrapping both hooks and components in custom providers as well as our base providers (localization, query, etc) and a valid Tanstack Router instance. Here's what is working for us (so far): renderWithProviders (for components)
Usage
renderHookWithProviders (for hooks)
Usage
|
Beta Was this translation helpful? Give feedback.
-
I just finished migrating our app from This should work for any project following the file-based routing approach (I haven't tested the others). However, there's a few things to keep in mind:
Now that that's out of the way, here's the code: // vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
TanStackRouterVite({
routeTreeFileFooter: [
"export type RouteId = Exclude<keyof FileRoutesById, '__root__'>;",
'export type Route = FileRoutesById[RouteId];',
'export type RouteTree = typeof routeTree;',
],
// ...
}),
// ...
],
// ...
}); // TestRouterProvider.tsx
import { FunctionComponent, ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
AnyRoute,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
ErrorComponent,
RootRoute,
RouterProvider,
} from '@tanstack/react-router';
// You'll need to modify your Vite config to have access to these types.
import { type Route, type RouteTree, routeTree } from '@src/routeTree.gen';
const getRouteChildren = (route: Route | RouteTree, parentRoute: AnyRoute): AnyRoute[] => {
return Object.values(route.children ?? {}).map((childRoute: AnyRoute) => {
if (!('path' in childRoute.options)) {
// This should never happen!
throw new Error(
`Found a route without a path. All routes must have a path. Route options: ${JSON.stringify(childRoute.options)}`
);
}
const testRoute = createRoute({
path: childRoute.options.path,
getParentRoute: () => parentRoute,
});
testRoute.addChildren(getRouteChildren(childRoute, testRoute));
return testRoute;
});
};
let cachedRootRoute: RootRoute | null = null;
export const buildTestRouteTree = (): RootRoute => {
if (cachedRootRoute) {
return cachedRootRoute;
}
const rootRoute = createRootRoute();
rootRoute.addChildren(getRouteChildren(routeTree, rootRoute));
// Cache the test route tree. TODO: Figure out how to get this cache shared across test files. Perhaps https://vitest.dev/config/#provide?
cachedRootRoute = rootRoute;
return rootRoute;
};
type RoutePath = Route['path'];
type Split<S extends string, D extends string> = string extends S
? string[]
: S extends ''
? []
: S extends `${infer T}${D}${infer U}`
? [T, ...Split<U, D>]
: [S];
type ExtractParams<Path extends RoutePath> = {
[T in Split<Path, '/'>[number] as T extends `$${infer U}`
? U extends ''
? // Any params for catchAll routes. e.g. "/some/path/$param/$ would
// result in the following type { param: string, [key:string]: string }
string
: U
: never]: string;
};
type PathEntry<Path extends RoutePath> = (keyof ExtractParams<Path> extends never
? { path: Path }
: // TODO: Find a way to make search type safe.
{ path: Path; params: ExtractParams<Path> }) & { search?: Record<string, string> };
// Required otherwise the union type is not inferred correctly (union of unique objects vs object with union values)
type PathEntries<Path extends RoutePath> = Path extends string ? PathEntry<Path> : never;
/**
* The initial entries for the router history. Each entry should contain a path and its respective
* params and search params if they exist. If the route has a catchAll path then the
* params will be set in the order they appear in the params object.
*/
export type InitialEntries = Array<PathEntries<RoutePath>>;
const replacePlaceholders = (path: string, params: Record<string, string>): string => {
const replacementParams = { ...params };
let url = path.replace(/\$([a-zA-Z0-9_]+)/g, (_, key: string) => {
const replacement = replacementParams[key];
if (!replacement) {
throw new Error(`Missing replacement for placeholder: ${key}`);
}
delete replacementParams[key];
return replacement;
});
// If there are any remaining params (catchAll route), append them to the end of the URL
for (const key in replacementParams) {
url += `/${replacementParams[key]}`;
}
return url;
};
const getRouterEntries = (initialEntries: InitialEntries): string[] => {
return initialEntries.map((entry) => {
const { path, search } = entry;
const needsReplace = 'params' in entry;
const stringSearch = new URLSearchParams(search).toString();
if (!needsReplace) {
return `${path}?${stringSearch}`;
}
const fullPath = replacePlaceholders(path, entry.params);
return `${fullPath}?${stringSearch}`;
});
};
interface TestRouterProviderProps {
children: ReactNode;
client?: QueryClient;
initialEntries?: InitialEntries;
}
export const TestRouterProvider: FunctionComponent<TestRouterProviderProps> = ({
children,
client,
initialEntries = [{ path: '/' }],
}) => {
const queryClient = client ?? new QueryClient();
const memoryHistory = createMemoryHistory({
initialEntries: getRouterEntries(initialEntries),
});
const router = createRouter({
routeTree: buildTestRouteTree(),
context: {
queryClient,
},
defaultComponent: () => children,
// Mute "wasn't caught by any route" errors. Ideally we would have a way to disable
// all error handling here so we could assert on components throwing errors.
// Overriding this component to try and get rid of the root error boundary does
// not work.
defaultErrorComponent: ErrorComponent,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
history: memoryHistory,
});
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}; // test-utils/render.tsx
import { ReactElement, ReactNode } from 'react';
import { QueryClient } from '@tanstack/react-query';
import {
RenderOptions,
render as baseRender,
} from '@testing-library/react';
import { InitialEntries, TestRouterProvider } from './TestRouterProvider';
interface WrapperProps {
children: ReactNode;
client?: QueryClient;
initialEntries?: InitialEntries;
}
export const wrapper = ({ children, client, initialEntries }: WrapperProps) => {
return (
<TestRouterProvider client={client} initialEntries={initialEntries}>
{children}
</TestRouterProvider>
);
};
export const render = (
ui: ReactElement,
options?: RenderOptions & {
initialEntries?: InitialEntries;
client?: QueryClient;
}
) =>
baseRender(ui, {
...options,
wrapper: ({ children }) => {
const Wrapper = wrapper;
const OptionsWrapper = options?.wrapper;
if (OptionsWrapper) {
return (
<Wrapper initialEntries={options.initialEntries} client={options.client}>
<OptionsWrapper>{children}</OptionsWrapper>
</Wrapper>
);
}
return (
<Wrapper initialEntries={options?.initialEntries} client={options?.client}>
{children}
</Wrapper>
);
},
}); Now you can write your tests just like you would with // GoBackButton.tsx
import { Link, useSearch } from '@tanstack/react-router';
export const GoBackButton = () => {
const search = useSearch({
strict: false,
});
return (
// Note the directory syntax
<Link to=".." search={search}>
{GO_BACK}
</Link>
);
}; // GoBackButton.test.tsx
import { screen } from '@testing-library/react';
import { render } from '@src/test-utils';
import { BackButton } from './BackButton';
describe('GoBackButton', () => {
it('includes search params in the URL if they exist', () => {
render(<BackButton />, {
initialEntries: [
{
path: '/users/$username/teams/$team',
params: {
username: 'rmzNadir',
team: 'TanStack',
},
search: {
q: 'search',
},
},
],
});
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/users/rmzNadir/teams?q=search'); // Passes ✅
});
}); I hope this helps someone! If you have any questions or suggestions, please feel free to reach out. I'm sure there's plenty of room for improvement here. 😺 |
Beta Was this translation helpful? Give feedback.
-
Hi Team,
I am trying to test my component and a hook with a vitest and RTL.
How to pass the Tanstack Router as a wrapper? Can you please help me on this.
I have got on idea on testing a Component from the following discussion 604 but still this code helps only for the component testing.
I don't know how to test a custom Hook or redux stores and sagas that has a Tanstack Router wrapper around it.
Is there any way that I can provide the Tanstak Router as a wrapper for the render method?
Beta Was this translation helpful? Give feedback.
All reactions