I'm trying to build a convenience function that allows me to pass a react component and have it render the component with all of the options and route specifications that I want that I pass it wrapped around a <BrowserRouter>
for unit testing.
The problem is when I try to pass Wrapper
function into react testing library's render()
(see rtlRender()
below), I'm getting an error from TypeScript stating the following
No overload matches this call. Overload 1 of 2, '(ui: ReactElement<any, string | JSXElementConstructor>, options: RenderOptions<Queries, HTMLElement>): RenderResult<...>', gave the following error. Type '({ children }: Props) => JSX.Element' is not assignable to type 'ComponentType<{}> | undefined'. Type '({ children }: Props) => JSX.Element' is not assignable to type 'FunctionComponent<{}>'. Types of parameters '__0' and 'props' are incompatible. Type '{ children?: ReactNode; }' is not assignable to type 'Props'. Types of property 'children' are incompatible. Type 'ReactNode' is not assignable to type 'ReactChildren'. Type 'undefined' is not assignable to type 'ReactChildren'. Overload 2 of 2, '(ui: ReactElement<any, string | JSXElementConstructor>, options?: Omit<RenderOptions<typeof import("/Users/myuser/dev/myapp/container/node_modules/@testing-library/dom/types/queries"), HTMLElement>, "queries"> | undefined): RenderResult<...>', gave the following error. Type '({ children }: Props) => JSX.Element' is not assignable to type 'ComponentType<{}> | undefined'. Type '({ children }: Props) => JSX.Element' is not assignable to type 'FunctionComponent<{}>'.ts(2769) index.d.ts(41, 3): The expected type comes from property 'wrapper' which is declared here on type 'RenderOptions<Queries, HTMLElement>' (property) RenderOptions<Queries, HTMLElement>.wrapper?: React.ComponentType<{}> | undefined
Here's my code
import React from 'react';
import { Queries, render as rtlRender } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { RenderResult } from '@testing-library/react/types';
/* Type Definitions */
type Route = {
state: Record<string, unknown>;
title: string;
location: string;
};
type Props = {
children: React.ReactChildren;
};
/* Helper Functions */
export function render(ui: JSX.Element, options: { route: Route }): RenderResult<Queries, HTMLElement> {
const { route } = options;
window.history.pushState(route?.state ?? null, route?.title ?? 'Root', route?.location ?? '/');
const Wrapper = ({ children }: Props) => {
return <BrowserRouter>{children}</BrowserRouter>;
};
return rtlRender(ui, { wrapper: Wrapper, ...options });
}
From what I understand, it's telling me that whatever Wrapper()
returns doesn't fit the description of a React class component or functional component. This seems weird to me because you can tell from the code, Wrapper()
should return a functional component.
I tried to assign Wrapper()
to type React.FC<Props>
, but TypeScript states that React.FC<Props>
is not of type React.ComponentType
which made me even more confused.
I'm still new to TypeScript so throwing in all these React types is confusing me even more. I'm hoping for some guidance on this.
React.FC
type is alias of React.FunctionComponent
type. The props of React.FunctionComponent
is PropsWithChildren
type which has children?: ReactNode
property. You don't need to create your own Props
type.
The options
parameter type of render
function is:
export interface RenderOptions<Q extends Queries = typeof queries> {
container?: Element
baseElement?: Element
hydrate?: boolean
queries?: Q
wrapper?: React.ComponentType
}
There is no route
property, I think it's for the custom render
function. But you need to use Intersection Types so that you can pass the options
to render
function of RTL.
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
type PropsWithChildren<P> = P & { children?: ReactNode };
The default generic parameter type for RenderResult
is typeof queries
, NOT Queries
. This is the Queries
interface:
export interface Queries {
[T: string]: Query;
}
It's totally different with @testing-library/dom/types/queries.d.ts
.
import React from 'react';
import { queries, render as rtlRender, RenderOptions } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { RenderResult } from '@testing-library/react/types';
/* Type Definitions */
type Route = {
state: Record<string, unknown>;
title: string;
location: string;
};
/* Helper Functions */
export function render(
ui: React.ReactElement,
options: { route: Route } & Omit<RenderOptions, 'queries'>
): RenderResult<typeof queries> {
const { route, ...rest } = options;
window.history.pushState(route?.state ?? null, route?.title ?? 'Root', route?.location ?? '/');
const Wrapper: React.FC = ({ children }) => {
return <BrowserRouter>{children}</BrowserRouter>;
};
return rtlRender(ui, { wrapper: Wrapper, ...rest });
}