Open slorber opened 5 years ago
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
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: "test@test.com",
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.
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:
I'd find this more idiomatic to do:
This would be quite similar to the of react-router