rfcs
rfcs copied to clipboard
<StaticNavigator> for tests/storybook
Hi,
I have screens on which some components are "connected" to react-navigation through withNavigation() hoc (a screen navbar header actually)
I want to be able to render these components in envs like tests or RN storybook, but by default it fails because no navigation is found in React context.
I don't really care if when pressing the backbutton it does not really do anything (ie the navigation action is ignored because unknown), as long as it renders.
The current workaround does not look very idiomatic, and I think having an official support for this feature could be helpful
Here's my solution for Storybook:
const reactNavigationDecorator: StoryDecorator = story => {
const Screen = () => story();
const Navigator = createAppContainer(createSwitchNavigator({ Screen }))
return <Navigator />
}
storiesOf('ProfileContext/Referal', module)
.addDecorator(reactNavigationDecorator)
.add('Referal', () => <ReferalDumb />)
I'd find this more idiomatic to do:
storiesOf('ProfileContext/Referal', module)
.addDecorator(reactNavigationDecorator)
.add('Referal', () => (
<StaticNavigator>
<ReferalDumb />
</StaticNavigator>
))
This would be quite similar to the <StaticRouter/> of react-router
How would you provide navigation state params to both the createAppContainer(createSwitchNavigator({ Screen })) version (so I can use it now) and the <StaticNavigator> proposed version?
If you don't care about the HOC, wouldn't it be better to mock it?
I got this component working:
const TestNavigator = ({
children,
params,
}: {
children: NavigationComponent;
params?: NavigationParams;
}) => {
const Navigator = createAppContainer(
createSwitchNavigator({
TestScreen: { screen: () => Children.only(children), params },
}),
);
return <Navigator />;
};
Then you can use it in react-native-testing-library's render function:
render(
<TestNavigator params={{ fakeParam: true }}>
<SomeComponent />
</TestNavigator>,
}
My motivation was to get the useNavigationParam hook working which is why just passing a navigation prop didn't work for testing.
Here's a full example of the testing helpers I wrote to wrap the tested component in a navigator and a redux provider: (used this for inspiration for the redux stuff)
import React, { ReactElement, Children } from 'react';
import 'react-native';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { render } from 'react-native-testing-library';
import configureStore, { MockStore } from 'redux-mock-store';
import {
createAppContainer,
createSwitchNavigator,
NavigationComponent,
NavigationParams,
} from 'react-navigation';
export const createThunkStore = configureStore([thunk]);
const TestNavigator = ({
children,
params,
}: {
children: NavigationComponent;
params?: NavigationParams;
}) => {
const Navigator = createAppContainer(
createSwitchNavigator({
TestScreen: { screen: () => Children.only(children), params },
}),
);
return <Navigator />;
};
interface RenderWithContextParams {
initialState?: {} | undefined;
store?: MockStore;
navParams?: NavigationParams;
}
export function renderWithContext(
component: ReactElement,
{
initialState,
store = createThunkStore(initialState),
navParams,
}: RenderWithContextParams = {},
) {
return {
...render(
<TestNavigator params={navParams}>
<Provider store={store}>{component}</Provider>
</TestNavigator>,
),
store,
};
}
export function snapshotWithContext(
component: ReactElement,
renderWithContextParams?: RenderWithContextParams,
) {
const { toJSON } = renderWithContext(component, renderWithContextParams);
expect(toJSON()).toMatchSnapshot();
}
Not sure how I feel about the renderWithContext name but I was trying to communicate that it wraps the component in Providers or provides context.
Great job, that's what I'd like to be implemented, + some other details like providing navigationOptions and other things.
@satya164 mocking certainly has some advantages like ability to test the RN integration and see what navigation methods are called etc.
But for usecases like adding a whole screen to storybook, where you might have several little compos (already tested independently with a mock) coupled to RN, it can be annoying to mock the navigation again. When you start to use things like useNavigationEvents and other hooks, you need to add more and more implementation to your mock. Having a <StaticNavigator/> comp is IMHO a simpler path already adopted by libraries like ReactRouter
Doing this throws a bunch of duplicate navigator warnings...
Added this as a Jest setup file:
const originalWarn = console.warn;
beforeAll(() => {
console.warn = (...args: any[]) => {
if (
/You should only render one navigator explicitly in your app, and other navigators should be rendered by including them in that navigator/.test(
args[0],
)
) {
return;
}
originalWarn.call(console, ...args);
};
});
afterAll(() => {
console.warn = originalWarn;
});
I discovered my TestNavigator doesn't work for rerender/update (I'm using react-native-testing-library). Since a new navigation instance is created with every call to createAppContainer, the whole tree rerenders, not just the component under test.
Here's a new version. It feels hacky. My new function renders a navigator and returns the createAppContainer's navigation prop. Then you can use that and the NavigationProvider for rendering. I'd love feedback or thoughts about adding some sort of test navigator to the library.
navigationHelpers.tsx:
import React from 'react';
import {
createAppContainer,
createSwitchNavigator,
NavigationParams,
NavigationScreenProp,
} from 'react-navigation';
import { render } from 'react-native-testing-library';
export const createNavigationProp = (params?: NavigationParams) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let navigationProp: NavigationScreenProp<any> | undefined;
const Navigator = createAppContainer(
createSwitchNavigator({
TestScreen: {
screen: ({
navigation,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navigation: NavigationScreenProp<any>;
}) => {
navigationProp = navigation;
return null;
},
params,
},
}),
);
render(<Navigator />);
if (navigationProp === undefined) {
throw 'Unable to get navigation screen prop';
}
return navigationProp;
};
Your test:
import React from 'react';
import { Text } from 'react-native';
import { render } from 'react-native-testing-library';
import { NavigationParams } from 'react-navigation';
import { NavigationProvider } from '@react-navigation/core';
import { useNavigationParam } from 'react-navigation-hooks';
import { createNavigationProp } from './navigationHelpers';
const ComponentUnderTest = () => {
const testParam = useNavigationParam('testParam');
return <Text>{testParam}</Text>;
};
it('should render correctly', () => {
const navParams: NavigationParams = { testParam: 'Test' };
const navigation = createNavigationProp(navParams);
const { toJSON } = render(
<NavigationProvider value={navigation}>
<ComponentUnderTest />
</NavigationProvider>,
);
expect(toJSON()).toMatchInlineSnapshot(`
<Text>
Test
</Text>
`);
});
There's another issue I just thought of with something like StaticNavigator. There are several navigators such as stack, tab, drawer etc. Each provide additional helpers (such as push for stack, jumpTo for tabs etc). A static navigator won't be able to provide correct helpers depending on the screen, so will break.
What might be more useful is to provide a context provider (e.g. NavigationMockProvider to provide mock implementations for the navigation prop (and the route prop for v5). We could provide default mocks for the core helpers so you don't need to write mocks for everything manually to make this easier (or even accept the router as a prop which auto-generates mocks for navigator specific helpers)
I think this way, we can support all different types navigators and help with both storybook and test scenarios.
That makes sense, didn't think about the navigation helpers. What about deriving NavigationMockProvider.Stack for example, if we want to provide mocks for popToTop automatically etc? (and let user override what he wants with jest.fn() if needed)
I haven't been able to find any documentation on recommended practices for using react-navigation v5 with storybook.
Are the workarounds posted here still valid for the new version?
@andreialecu , it should work the same way. I haven't tested but you can try:
const reactNavigationDecorator = story => {
const Screen = () => story();
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="MyStorybookScreen" component={Screen} />
</Stack.Navigator>
<NavigationContainer>
)
}
Thanks @slorber, I ended up with the following:
const Stack = createStackNavigator();
const reactNavigationDecorator = story => {
const Screen = () => story();
return (
<NavigationContainer independent={true}>
<Stack.Navigator>
<Stack.Screen name="MyStorybookScreen" component={Screen} options={{header: () => null}} />
</Stack.Navigator>
</NavigationContainer>
)
}
addDecorator(reactNavigationDecorator);
I needed independent={true} because storybook apparently has a bug with the new Fast Refresh in RN 0.61+ and somehow the decorator keeps being re-added on code changes, and the following happens on each code change:

Not sure if react-navigation can do anything about this or it's just a StoryBook issue.
I was able to work around it by disabling the screen header via options={{header: () => null}}. Didn't see any issues otherwise.
The story itself would look like this:
storiesOf("Forms", module).add("Confirm Email", () => {
const navigation = useNavigation<
StackNavigationProp<AppStackParamList, "RegConfirm">
>();
const route = useRoute<RouteProp<AppStackParamList, "RegConfirm">>();
route.params = {
values: {
email: "[email protected]",
username: "tester",
password: "justtesting"
}
};
return (
<ConfirmForm
navigation={navigation}
route={route}
onSubmit={(values) => {
Alert.alert("", JSON.stringify(values, null, 2));
}}
/>
);
});
great to see it works for you ;)
~~Does anyone have any experience with a completely white screen in storybook after adding a Navigator decorator? I am using Storybook 6.2.x and RN5.~~
Basically the way I resolved this is by using webpack to mock useNavigation and useRoute rather than trying to mock all of the providers and scaffolding of react navigation.
Really simple stuff:
/.storybook/mocks/navigation.js:
export const useRoute = () => {
return {
name: 'fun route',
params: {
nothing: 'nice ocean view'
}
};
};
export const useNavigation = () => {
return {
push: () => null,
goBack: () => null,
pop: () => null,
popToTop: () => null,
reset: () => null,
replace: () => null,
navigate: () => null,
setParams: () => null,
jumpTo: () => null
};
};
Hey sir,
~Does anyone have any experience with a completely white screen in storybook after adding a Navigator decorator? I am using Storybook 6.2.x and RN5.~
Basically the way I resolved this is by using webpack to mock useNavigation and useRoute rather than trying to mock all of the providers and scaffolding of react navigation.
Really simple stuff:
/.storybook/mocks/navigation.js:
export const useRoute = () => { return { name: 'fun route', params: { nothing: 'nice ocean view' } }; }; export const useNavigation = () => { return { push: () => null, goBack: () => null, pop: () => null, popToTop: () => null, reset: () => null, replace: () => null, navigate: () => null, setParams: () => null, jumpTo: () => null }; };
Regarding this,
Basically the way I resolved this is by using webpack to mock useNavigation and useRoute
How do you this file ya? Can you share your webpack file as well?
I have tried in my /.storybook/main.js I do like this:
module.exports = {
// your Storybook configuration
webpackFinal: (config) => {
config.resolve.alias['@react-navigation/native'] = require.resolve('../__mocks__/navigation.js');
return config;
},
};
But it still didnt use the function in mock folder. And I will get this error:
Warning: Cannot update a component from inside the function body of a different component.