diff --git a/.gitignore b/.gitignore index a4985aa04..c77dbf4ce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ node_modules build .idea .DS_Store + +# Ignore lock files in examples for now +examples/**/yarn.lock diff --git a/docs/ReactNavigation.md b/docs/ReactNavigation.md new file mode 100644 index 000000000..0d4ced387 --- /dev/null +++ b/docs/ReactNavigation.md @@ -0,0 +1,215 @@ +--- +id: react-navigation +title: React Navigation +--- + +This section deals with integrating `react-native-testing-library` with `react-navigation`, using Jest. + +## Setting up + +Install the packages required for React Navigation. For this example, we will use a [stack navigator](https://reactnavigation.org/docs/stack-navigator/) to transition to the second page when any of the items are clicked on. + +``` +$ yarn add @react-native-community/masked-view @react-navigation/native @react-navigation/stack react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-screens +``` + +Create an [`./AppNavigator.js`](https://github.com/callstack/react-native-testing-library/blob/master/examples/reactnavigation/src/AppNavigator.js) component which will list the navigation stack: + +```jsx +import 'react-native-gesture-handler'; +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; + +import HomeScreen from './screens/HomeScreen'; +import DetailsScreen from './screens/DetailsScreen'; + +const { Screen, Navigator } = createStackNavigator(); + +export default function Navigation() { + const options = {}; + + return ( + + + + + ); +} +``` + +Create your two screens which we will transition to and from them. The homescreen, found in [`./screens/HomeScreen.js`](https://github.com/callstack/react-native-testing-library/blob/master/examples/reactnavigation/src/screens/HomeScreen.js), contains a list of elements presented in a list view. On tap of any of these items will move to the details screen with the item number: + +```jsx +import React from 'react'; +import { + Text, + View, + FlatList, + TouchableOpacity, + StyleSheet, +} from 'react-native'; + +export default function HomeScreen({ navigation }) { + const [items] = React.useState( + new Array(20).fill(null).map((_, idx) => idx + 1) + ); + + const onOpacityPress = item => navigation.navigate('Details', item); + + return ( + + List of numbers from 1 to 20 + `${idx}`} + data={items} + renderItem={({ item }) => ( + onOpacityPress(item)} + style={styles.row} + > + Item number {item} + + )} + /> + + ); +} + +const divider = '#DDDDDD'; + +const styles = StyleSheet.create({ + header: { + fontSize: 20, + textAlign: 'center', + marginVertical: 16, + }, + row: { + paddingVertical: 16, + paddingHorizontal: 24, + borderBottomColor: divider, + borderBottomWidth: 1, + }, +}); +``` + +The details screen, found in [`./screens/DetailsScreen.js`](https://github.com/callstack/react-native-testing-library/blob/master/examples/reactnavigation/src/screens/DetailsScreen.js), contains a header with the item number passed from the home screen: + +```jsx +// ./screens/DetailsScreen.js +import React from 'react'; +import { Text, StyleSheet, View } from 'react-native'; + +export default function DetailsScreen(props) { + const item = Number.parseInt(props.route.params, 10); + + return ( + + Showing details for {item} + the number you have chosen is {item} + + ); +} + +const styles = StyleSheet.create({ + header: { + fontSize: 20, + textAlign: 'center', + marginVertical: 16, + }, + body: { + textAlign: 'center', + }, +}); +``` + +## Setting up the test environment + +Install required dev dependencies: + +``` +$ yarn add -D jest react-native-testing-library +``` + +Create your `jest.config.js` file (or place the following properties in your `package.json` as a "jest" property) + +```js +module.exports = { + preset: 'react-native', + setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'], + transformIgnorePatterns: [ + 'node_modules/(?!(jest-)?react-native|@react-native-community|@react-navigation)', + ], +}; +``` + +Notice the 2 entries that don't come with the default React Native project: + +- `setupFiles` – an array of files that Jest is going to execute before running your tests. In this case, we run `react-native-gesture-handler/jestSetup.js` which sets up necessary mocks for `react-native-gesture-handler` native module +- `transformIgnorePatterns` – an array of paths that Jest ignores when transforming code. In this case, the negative lookahead regular expression is used, to tell Jest to transform (with Babel) every package inside `node_modules/` that starts with `react-native`, `@react-native-community` or `@react-navigation` (added by us, the rest is in `react-native` preset by default, so you don't have to worry about it). + +For this example, we are going to test out two things. The first thing is that the page is laid out as expected. The second, and most important, is that the page will transition to the detail screen when any item is tapped on. + +Let's a [`AppNavigator.test.js`](https://github.com/callstack/react-native-testing-library/blob/master/examples/reactnavigation/src/__tests__/AppNavigator.js) file in `src/__tests__` directory: + +```jsx +import React from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { render, fireEvent, cleanup } from 'react-native-testing-library'; + +import AppNavigator from '../AppNavigator'; + +// Silence the warning https://github.com/facebook/react-native/issues/11094#issuecomment-263240420 +jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper'); + +describe('Testing react navigation', () => { + afterEach(cleanup); + + test('page contains the header and 10 items', () => { + const component = ( + + + + ); + + const { getByText, getAllByText } = render(component); + + const header = getByText('List of numbers from 1 to 20'); + const items = getAllByText(/Item number/); + + expect(header).toBeTruthy(); + expect(items.length).toBe(10); + }); + + test('clicking on one item takes you to the details screen', async () => { + const component = ( + + + + ); + + const { getByText } = render(component); + const toClick = getByText('Item number 5'); + + fireEvent(toClick, 'press'); + const newHeader = getByText('Showing details for 5'); + const newBody = getByText('the number you have chosen is 5'); + + expect(newHeader).toBeTruthy(); + expect(newBody).toBeTruthy(); + }); +}); +``` + +## Running tests + +To run the tests, place a test script inside your `package.json` + +```json +{ + "scripts": { + "test": "jest" + } +} +``` + +And run the `test` script with `npm test` or `yarn test`. diff --git a/examples/reactnavigation/babel.config.js b/examples/reactnavigation/babel.config.js new file mode 100644 index 000000000..f842b77fc --- /dev/null +++ b/examples/reactnavigation/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/examples/reactnavigation/jest.config.js b/examples/reactnavigation/jest.config.js new file mode 100644 index 000000000..e8fd9d396 --- /dev/null +++ b/examples/reactnavigation/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'react-native', + setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'], + transformIgnorePatterns: [ + 'node_modules/(?!(jest-)?react-native|@react-native-community|@react-navigation)', + ], +}; diff --git a/examples/reactnavigation/package.json b/examples/reactnavigation/package.json new file mode 100644 index 000000000..c2428b592 --- /dev/null +++ b/examples/reactnavigation/package.json @@ -0,0 +1,30 @@ +{ + "name": "react-navigation-example", + "description": "Testing react-navigation with react-native-testing-library", + "version": "0.0.1", + "private": true, + "scripts": { + "test": "jest" + }, + "dependencies": { + "@react-native-community/masked-view": "^0.1.9", + "@react-navigation/native": "^5.1.6", + "@react-navigation/stack": "^5.2.13", + "prop-types": "^15.7.2", + "react": "^16.13.1", + "react-native": "^0.62.2", + "react-native-gesture-handler": "^1.6.1", + "react-native-reanimated": "^1.8.0", + "react-native-safe-area-context": "^0.7.3", + "react-native-screens": "^2.5.0" + }, + "devDependencies": { + "@babel/core": "^7.9.0", + "@babel/runtime": "^7.9.2", + "babel-jest": "^25.4.0", + "jest": "^25.4.0", + "metro-react-native-babel-preset": "^0.59.0", + "react-native-testing-library": "^1.13.0", + "react-test-renderer": "^16.13.1" + } +} diff --git a/examples/reactnavigation/src/App.js b/examples/reactnavigation/src/App.js new file mode 100644 index 000000000..288feadf4 --- /dev/null +++ b/examples/reactnavigation/src/App.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { StatusBar, StyleSheet, View } from 'react-native'; +import { NavigationContainer } from '@react-navigation/native'; + +import AppNavigator from './AppNavigator'; + +export default function App() { + return ( + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); diff --git a/examples/reactnavigation/src/AppNavigator.js b/examples/reactnavigation/src/AppNavigator.js new file mode 100644 index 000000000..e0510ce60 --- /dev/null +++ b/examples/reactnavigation/src/AppNavigator.js @@ -0,0 +1,19 @@ +import 'react-native-gesture-handler'; +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; + +import HomeScreen from './screens/HomeScreen'; +import DetailsScreen from './screens/DetailsScreen'; + +const { Screen, Navigator } = createStackNavigator(); + +export default function Navigation() { + const options = {}; + + return ( + + + + + ); +} diff --git a/examples/reactnavigation/src/__tests__/AppNavigator.test.js b/examples/reactnavigation/src/__tests__/AppNavigator.test.js new file mode 100644 index 000000000..b2f4a85c1 --- /dev/null +++ b/examples/reactnavigation/src/__tests__/AppNavigator.test.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { render, fireEvent, cleanup } from 'react-native-testing-library'; + +import AppNavigator from '../AppNavigator'; + +// Silence the warning https://github.com/facebook/react-native/issues/11094#issuecomment-263240420 +jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper'); + +describe('Testing react navigation', () => { + afterEach(cleanup); + + test('page contains the header and 10 items', () => { + const component = ( + + + + ); + + const { getByText, getAllByText } = render(component); + + const header = getByText('List of numbers from 1 to 20'); + const items = getAllByText(/Item number/); + + expect(header).toBeTruthy(); + expect(items.length).toBe(10); + }); + + test('clicking on one item takes you to the details screen', async () => { + const component = ( + + + + ); + + const { getByText } = render(component); + const toClick = getByText('Item number 5'); + + fireEvent(toClick, 'press'); + const newHeader = getByText('Showing details for 5'); + const newBody = getByText('the number you have chosen is 5'); + + expect(newHeader).toBeTruthy(); + expect(newBody).toBeTruthy(); + }); +}); diff --git a/examples/reactnavigation/src/screens/DetailsScreen.js b/examples/reactnavigation/src/screens/DetailsScreen.js new file mode 100644 index 000000000..0f7ed93b7 --- /dev/null +++ b/examples/reactnavigation/src/screens/DetailsScreen.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { Text, StyleSheet, View } from 'react-native'; + +export default function DetailsScreen(props) { + const item = Number.parseInt(props.route.params, 10); + + return ( + + Showing details for {item} + the number you have chosen is {item} + + ); +} + +const styles = StyleSheet.create({ + header: { + fontSize: 20, + textAlign: 'center', + marginVertical: 16, + }, + body: { + textAlign: 'center', + }, +}); diff --git a/examples/reactnavigation/src/screens/HomeScreen.js b/examples/reactnavigation/src/screens/HomeScreen.js new file mode 100644 index 000000000..ac1aa831e --- /dev/null +++ b/examples/reactnavigation/src/screens/HomeScreen.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { + Text, + View, + FlatList, + TouchableOpacity, + StyleSheet, +} from 'react-native'; + +export default function HomeScreen({ navigation }) { + const [items] = React.useState( + new Array(20).fill(null).map((_, idx) => idx + 1) + ); + + const onOpacityPress = item => navigation.navigate('Details', item); + + return ( + + List of numbers from 1 to 20 + `${idx}`} + data={items} + renderItem={({ item }) => ( + onOpacityPress(item)} + style={styles.row} + > + Item number {item} + + )} + /> + + ); +} + +const divider = '#DDDDDD'; + +const styles = StyleSheet.create({ + header: { + fontSize: 20, + textAlign: 'center', + marginVertical: 16, + }, + row: { + paddingVertical: 16, + paddingHorizontal: 24, + borderBottomColor: divider, + borderBottomWidth: 1, + }, +}); diff --git a/package.json b/package.json index 28a7e4b39..c6f514cac 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "moduleFileExtensions": [ "js", "json" - ] + ], + "rootDir": "./src" }, "greenkeeper": { "ignore": [ diff --git a/website/i18n/en.json b/website/i18n/en.json index 1a1e5c3f8..42e143e8f 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -13,6 +13,9 @@ }, "api-queries": { "title": "Queries" + }, + "react-navigation": { + "title": "React Navigation" } }, "links": { @@ -21,7 +24,8 @@ }, "categories": { "Introduction": "Introduction", - "API Reference": "API Reference" + "API Reference": "API Reference", + "Examples": "Examples" } }, "pages-strings": { diff --git a/website/sidebars.json b/website/sidebars.json index 56e08cbd5..afa2cb112 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -1,6 +1,7 @@ { "docs": { "Introduction": ["getting-started"], - "API Reference": ["api", "api-queries"] + "API Reference": ["api", "api-queries"], + "Examples": ["react-navigation"] } } diff --git a/website/siteConfig.js b/website/siteConfig.js index 19fd95803..2c4ea5065 100644 --- a/website/siteConfig.js +++ b/website/siteConfig.js @@ -84,7 +84,7 @@ const siteConfig = { // template. For example, if you need your repo's URL... repoUrl, usePrism: ['jsx'], - + // search algolia: { apiKey: 'cd9b8b73f97b64ed04570e41c507683f', diff --git a/website/static/css/custom.css b/website/static/css/custom.css index 8e44ea944..655aab7de 100644 --- a/website/static/css/custom.css +++ b/website/static/css/custom.css @@ -18,3 +18,11 @@ a { color: #bf8a65; } + +.button-container { + margin-bottom: 2rem; +} + +.sandbox { + background-color: blue; +}