I'm building a React Native app with TypeScript. For my navigation I use React Navigation and for my unit testing I use Jest and Enzyme.
Here is the (stripped down) code for one of my screen (LoadingScreen.tsx):
import styles from "./styles";
import React, { Component } from "react";
import { Text, View } from "react-native";
import { NavigationScreenProps } from "react-navigation";
// Is this correct?
export class LoadingScreen extends Component<NavigationScreenProps> {
// Or should I've done:
// export interface Props {
// navigation: NavigationScreenProp<any, any>;
// }
// export class LoadingScreen extends Component<Props> {
componentDidMount = () => {
this.props.navigation.navigate("LoginScreen");
};
render() {
return (
<View style={styles.container}>
<Text>This is the LoadingScreen.</Text>
</View>
);
}
}
export default LoadingScreen;
When trying to test the screens I came across a problem. The screens expects a prop with a type of NavigiationScreenProps because I'm accessing React Navigations navigation
prop. Here is the testing file's code (LoadingScreen.test.tsx):
import { LoadingScreen } from "./LoadingScreen";
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import { View } from "react-native";
import * as navigation from "react-navigation";
const createTestProps = (props: Object) => ({
...props
});
describe("LoadingScreen", () => {
describe("rendering", () => {
let wrapper: ShallowWrapper;
let props: Object;
beforeEach(() => {
props = createTestProps({});
wrapper = shallow(<LoadingScreen {...props} />);
});
it("should render a <View />", () => {
expect(wrapper.find(View)).toHaveLength(1);
});
});
});
The problem is, that LoadingScreen
expects a navigation
prop.
I get the error:
[ts]
Type '{ constructor: Function; toString(): string; toLocaleString(): string; valueOf(): Object; hasOwnProperty(v: string | number | symbol): boolean; isPrototypeOf(v: Object): boolean; propertyIsEnumerable(v: string | ... 1 more ... | symbol): boolean; }' is not assignable to type 'Readonly<NavigationScreenProps<NavigationParams, any>>'.
Property 'navigation' is missing in type '{ constructor: Function; toString(): string; toLocaleString(): string; valueOf(): Object; hasOwnProperty(v: string | number | symbol): boolean; isPrototypeOf(v: Object): boolean; propertyIsEnumerable(v: string | ... 1 more ... | symbol): boolean; }'.
(alias) class LoadingScreen
How can I fix this?
I think I somehow have to mock the navigation
prop. I tried doing that (as you can see I imported *
from React Navigation in my test), but couldn't figure out. There is only NavigationActions
that is remotely useful but it only includes navigate()
. TypeScript expects everything, even the state, to be mocked. How can I mock the navigation
prop?
Edit 1: Is the approach of using NavigationScreenProps
even correct or should I use the interface Props
approach? If yes how would you then mock than (it results in the same error).
Edit 2: Using the second approach with the interface and
export class LoadingScreen extends Component<Props, object>
I was able to "solve" this problem. I literally had to mock every single property of the navigation object like this:
const createTestProps = (props: Object) => ({
navigation: {
state: { params: {} },
dispatch: jest.fn(),
goBack: jest.fn(),
dismiss: jest.fn(),
navigate: jest.fn(),
openDrawer: jest.fn(),
closeDrawer: jest.fn(),
toggleDrawer: jest.fn(),
getParam: jest.fn(),
setParams: jest.fn(),
addListener: jest.fn(),
push: jest.fn(),
replace: jest.fn(),
pop: jest.fn(),
popToTop: jest.fn(),
isFocused: jest.fn()
},
...props
});
The question remains: Is this correct? Or is there a better solution?
Edit 3: Back when I used JS, it was enough to mock only the property I needed (often just navigate). But since I started using TypeScript, I had to mock every single aspects of navigation. Otherwise TypeScript would complain that the component expects a prop with a different type.
The mock does not match the expected type so TypeScript reports an error.
You can use the type any
"to opt-out of type-checking and let the values pass through compile-time checks".
As you mentioned, in JavaScript it works to mock only what is needed for the test.
In TypeScript the same mock will cause an error since it does not completely match the expected type.
In situations like these where you have a mock that you know does not match the expected type you can use any
to allow the mock to pass through compile-time checks.
Here is an updated test:
import { LoadingScreen } from "./LoadingScreen";
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import { View } from "react-native";
const createTestProps = (props: Object) => ({
navigation: {
navigate: jest.fn()
},
...props
});
describe("LoadingScreen", () => {
describe("rendering", () => {
let wrapper: ShallowWrapper;
let props: any; // use type "any" to opt-out of type-checking
beforeEach(() => {
props = createTestProps({});
wrapper = shallow(<LoadingScreen {...props} />); // no compile-time error
});
it("should render a <View />", () => {
expect(wrapper.find(View)).toHaveLength(1); // SUCCESS
expect(props.navigation.navigate).toHaveBeenCalledWith('LoginScreen'); // SUCCESS
});
});
});