Skip to content

Commit 038ae4a

Browse files
feature: basic example app (#1025)
1 parent 7dd403b commit 038ae4a

File tree

17 files changed

+399
-0
lines changed

17 files changed

+399
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3+
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4+
}

examples/basic/.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
node_modules/
2+
.expo/
3+
dist/
4+
npm-debug.*
5+
*.jks
6+
*.p8
7+
*.p12
8+
*.key
9+
*.mobileprovision
10+
*.orig.*
11+
web-build/
12+
13+
# macOS
14+
.DS_Store

examples/basic/App.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from 'react';
2+
import { SafeAreaView } from 'react-native';
3+
import { LoginForm } from './components/LoginForm';
4+
import { Home } from './components/Home';
5+
6+
const App = () => {
7+
const [user, setUser] = React.useState<string | null>(null);
8+
9+
return (
10+
<SafeAreaView>
11+
{user == null ? (
12+
<LoginForm onLoginSuccess={setUser} />
13+
) : (
14+
<Home user={user} />
15+
)}
16+
</SafeAreaView>
17+
);
18+
};
19+
20+
export default App;

examples/basic/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Basic RNTL setup
2+
3+
This example is shows a basic modern React Native Testing Library setup in a template Expo app.
4+
5+
The app and related tests written in TypeScript, and it uses recommended practices like:
6+
7+
- testing large pieces of application instead of small components
8+
- using `screen`-based queries
9+
- using recommended query types, e.g. `byText`, `byLabelText`, `byPlaceholderText` over `byTestId`

examples/basic/__tests__/App.test.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import * as React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react-native';
3+
import App from '../App';
4+
5+
/**
6+
* A good place to start is having a tests that your component renders correctly.
7+
*/
8+
test('renders correctly', () => {
9+
// Idiom: no need to capture render output, as we will use `screen` for queries.
10+
render(<App />);
11+
12+
// Idiom: `getByXxx` is a predicate by itself, but we will use it with `expect().toBeTruthy()`
13+
// to clarify our intent.
14+
expect(screen.getByText('Sign in to Example App')).toBeTruthy();
15+
});
16+
17+
/**
18+
* Hint: It's best when your tests are similar to what a manual test scenarions would look like,
19+
* i.e. a series of actions taken by the user, followed by a series of assertions verified from
20+
* his point of view.
21+
*/
22+
test('User can sign in successully with correct credentials', async () => {
23+
// Idiom: no need to capture render output, as we will use `screen` for queries.
24+
render(<App />);
25+
26+
// Idiom: `getByXxx` is a predicate by itself, but we will use it with `expect().toBeTruthy()` to
27+
// clarify our intent.
28+
// Note: `.toBeTruthy()` is the preferred matcher for checking that elements are present.
29+
expect(screen.getByText('Sign in to Example App')).toBeTruthy();
30+
expect(screen.getByText('Username')).toBeTruthy();
31+
expect(screen.getByText('Password')).toBeTruthy();
32+
33+
// Hint: we can use `getByLabelText` to find our text inputs in accessibility-friendly way.
34+
fireEvent.changeText(screen.getByLabelText('Username'), 'admin');
35+
fireEvent.changeText(screen.getByLabelText('Password'), 'admin1');
36+
37+
// Hint: we can use `getByText` to find our button by its text.
38+
fireEvent.press(screen.getByText('Sign In'));
39+
40+
// Idiom: since pressing button triggers async operation we need to use `findBy` query to wait
41+
// for the action to complete.
42+
// Hint: subsequent queries do not need to use `findBy`, because they are used after the async action
43+
// already finished
44+
expect(await screen.findByText('Welcome admin!')).toBeTruthy();
45+
46+
// Idiom: use `queryByXxx` with `expect().toBeFalsy()` to assess that element is not present.
47+
expect(screen.queryByText('Sign in to Example App')).toBeFalsy();
48+
expect(screen.queryByText('Username')).toBeFalsy();
49+
expect(screen.queryByText('Password')).toBeFalsy();
50+
});
51+
52+
/**
53+
* Another test case based on manual test scenario.
54+
*
55+
* Hint: Try to tests what a user would see and do, instead of assering internal component state
56+
* that is not directly reflected in the UI.
57+
*
58+
* For this reason prefer quries that correspond to things directly observable by the user like:
59+
* `getByText`, `getByLabelText`, `getByPlaceholderText, `getByDisplayValue`, `getByRole`, etc.
60+
* over `getByTestId` which is not directly observable by the user.
61+
*
62+
* Note: that some times you will have to resort to `getByTestId`, but treat it as a last resort.
63+
*/
64+
test('User will see errors for incorrect credentials', async () => {
65+
render(<App />);
66+
67+
expect(screen.getByText('Sign in to Example App')).toBeTruthy();
68+
expect(screen.getByText('Username')).toBeTruthy();
69+
expect(screen.getByText('Password')).toBeTruthy();
70+
71+
fireEvent.changeText(screen.getByLabelText('Username'), 'admin');
72+
fireEvent.changeText(screen.getByLabelText('Password'), 'qwerty123');
73+
fireEvent.press(screen.getByText('Sign In'));
74+
75+
// Hint: you can use custom Jest Native matcher to check text content.
76+
expect(await screen.findByLabelText('Error')).toHaveTextContent(
77+
'Incorrect username or password'
78+
);
79+
80+
expect(screen.getByText('Sign in to Example App')).toBeTruthy();
81+
expect(screen.getByText('Username')).toBeTruthy();
82+
expect(screen.getByText('Password')).toBeTruthy();
83+
});
84+
85+
/**
86+
* Do not be afraid to write longer test scenarios, with repeating act and assert statements.
87+
*/
88+
test('User can sign in after incorrect attempt', async () => {
89+
render(<App />);
90+
91+
expect(screen.getByText('Sign in to Example App')).toBeTruthy();
92+
expect(screen.getByText('Username')).toBeTruthy();
93+
expect(screen.getByText('Password')).toBeTruthy();
94+
95+
fireEvent.changeText(screen.getByLabelText('Username'), 'admin');
96+
fireEvent.changeText(screen.getByLabelText('Password'), 'qwerty123');
97+
fireEvent.press(screen.getByText('Sign In'));
98+
99+
expect(await screen.findByLabelText('Error')).toHaveTextContent(
100+
'Incorrect username or password'
101+
);
102+
103+
fireEvent.changeText(screen.getByLabelText('Password'), 'admin1');
104+
fireEvent.press(screen.getByText('Sign In'));
105+
106+
expect(await screen.findByText('Welcome admin!')).toBeTruthy();
107+
expect(screen.queryByText('Sign in to Example App')).toBeFalsy();
108+
expect(screen.queryByText('Username')).toBeFalsy();
109+
expect(screen.queryByText('Password')).toBeFalsy();
110+
});

examples/basic/app.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"expo": {
3+
"name": "RNTL Example Basic",
4+
"slug": "rntl-example-basic",
5+
"version": "1.0.0",
6+
"orientation": "portrait",
7+
"icon": "./assets/icon.png",
8+
"userInterfaceStyle": "light",
9+
"splash": {
10+
"image": "./assets/splash.png",
11+
"resizeMode": "contain",
12+
"backgroundColor": "#ffffff"
13+
},
14+
"updates": {
15+
"fallbackToCacheTimeout": 0
16+
},
17+
"assetBundlePatterns": ["**/*"],
18+
"ios": {
19+
"supportsTablet": true
20+
},
21+
"android": {
22+
"adaptiveIcon": {
23+
"foregroundImage": "./assets/adaptive-icon.png",
24+
"backgroundColor": "#FFFFFF"
25+
}
26+
},
27+
"web": {
28+
"favicon": "./assets/favicon.png"
29+
}
30+
}
31+
}
17.1 KB
Loading

examples/basic/assets/favicon.png

1.43 KB
Loading

examples/basic/assets/icon.png

21.9 KB
Loading

examples/basic/assets/splash.png

46.2 KB
Loading

examples/basic/babel.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = function (api) {
2+
api.cache(true);
3+
return {
4+
presets: ['babel-preset-expo'],
5+
};
6+
};

examples/basic/components/Home.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as React from 'react';
2+
import { StyleSheet, View, Text } from 'react-native';
3+
4+
type Props = {
5+
user: string;
6+
};
7+
8+
export function Home({ user }: Props) {
9+
return (
10+
<View style={styles.container}>
11+
<Text style={styles.title}>Welcome {user}!</Text>
12+
</View>
13+
);
14+
}
15+
16+
const styles = StyleSheet.create({
17+
container: {
18+
padding: 20,
19+
},
20+
title: {
21+
alignSelf: 'center',
22+
fontSize: 24,
23+
marginTop: 8,
24+
marginBottom: 40,
25+
},
26+
});
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as React from 'react';
2+
import {
3+
StyleSheet,
4+
View,
5+
Text,
6+
TextInput,
7+
TouchableOpacity,
8+
ActivityIndicator,
9+
} from 'react-native';
10+
11+
type Props = {
12+
onLoginSuccess: (user: string) => void;
13+
};
14+
15+
export function LoginForm({ onLoginSuccess }: Props) {
16+
const [username, setUsername] = React.useState('');
17+
const [password, setPassword] = React.useState('');
18+
const [error, setError] = React.useState<string | undefined>();
19+
const [isLoading, setIsLoading] = React.useState(false);
20+
21+
const handleSignIn = async () => {
22+
setIsLoading(true);
23+
24+
const user = await authUser(username, password);
25+
setIsLoading(false);
26+
27+
if (user) {
28+
setError(undefined);
29+
onLoginSuccess(user);
30+
} else {
31+
setError('Incorrect username or password');
32+
}
33+
};
34+
35+
return (
36+
<View style={styles.container}>
37+
<Text style={styles.title}>Sign in to Example App</Text>
38+
39+
<Text style={styles.textLabel}>Username</Text>
40+
<TextInput
41+
value={username}
42+
onChangeText={setUsername}
43+
accessibilityLabel="Username"
44+
autoCapitalize="none"
45+
style={styles.textInput}
46+
/>
47+
48+
<Text style={styles.textLabel}>Password</Text>
49+
<TextInput
50+
value={password}
51+
onChangeText={setPassword}
52+
accessibilityLabel="Password"
53+
secureTextEntry={true}
54+
style={styles.textInput}
55+
/>
56+
57+
{error && (
58+
<Text accessibilityLabel="Error" style={styles.validator}>
59+
{error}
60+
</Text>
61+
)}
62+
63+
<TouchableOpacity onPress={handleSignIn} style={styles.button}>
64+
{isLoading ? (
65+
<ActivityIndicator color="white" />
66+
) : (
67+
<Text style={styles.buttonText}>Sign In</Text>
68+
)}
69+
</TouchableOpacity>
70+
</View>
71+
);
72+
}
73+
74+
/**
75+
* Fake authentication function according to our abilities.
76+
* @param username The username to authenticate.
77+
* @param password The password to authenticate.
78+
* @returns username if the username and password are correct, null otherwise.
79+
*/
80+
async function authUser(
81+
username: string,
82+
password: string
83+
): Promise<string | null> {
84+
return new Promise((resolve) =>
85+
setTimeout(() => {
86+
const hasValidCredentials = username === 'admin' && password === 'admin1';
87+
resolve(hasValidCredentials ? username : null);
88+
}, 250)
89+
);
90+
}
91+
92+
const styles = StyleSheet.create({
93+
container: {
94+
padding: 20,
95+
},
96+
title: {
97+
alignSelf: 'center',
98+
fontSize: 24,
99+
marginTop: 8,
100+
marginBottom: 40,
101+
},
102+
textLabel: {
103+
fontSize: 16,
104+
color: '#444',
105+
},
106+
textInput: {
107+
fontSize: 20,
108+
padding: 8,
109+
marginVertical: 8,
110+
borderColor: 'black',
111+
borderWidth: 1,
112+
},
113+
button: {
114+
backgroundColor: '#3256a8',
115+
padding: 16,
116+
alignItems: 'center',
117+
justifyContent: 'center',
118+
marginTop: 20,
119+
minHeight: 56,
120+
},
121+
buttonText: {
122+
fontSize: 20,
123+
fontWeight: '600',
124+
color: 'white',
125+
},
126+
validator: {
127+
color: 'red',
128+
fontSize: 18,
129+
marginTop: 8,
130+
},
131+
});

examples/basic/jest-setup.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* eslint-disable no-undef, import/no-extraneous-dependencies */
2+
3+
// Import Jest Native matchers
4+
import '@testing-library/jest-native/extend-expect';
5+
6+
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
7+
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

examples/basic/jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
preset: '@testing-library/react-native',
3+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
4+
setupFilesAfterEnv: ['./jest-setup.js'],
5+
};

0 commit comments

Comments
 (0)