Skip to content

Commit dc866d0

Browse files
committed
docs: basic cookbook docs and app
1 parent e418b6b commit dc866d0

29 files changed

+10781
-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/cookbook/.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# General Node.js
2+
node_modules/
3+
.expo/
4+
dist/
5+
npm-debug.*
6+
*.jks
7+
*.p8
8+
*.p12
9+
*.key
10+
*.mobileprovision
11+
*.orig.*
12+
web-build/
13+
14+
# Yarn 4.x
15+
.pnp.*
16+
.yarn/*
17+
!.yarn/patches
18+
!.yarn/plugins
19+
!.yarn/releases
20+
!.yarn/sdks
21+
!.yarn/versions
22+
23+
# macOS
24+
.DS_Store
25+

examples/cookbook/App.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 ? <LoginForm onLoginSuccess={setUser} /> : <Home user={user} />}
12+
</SafeAreaView>
13+
);
14+
};
15+
16+
export default App;

examples/cookbook/README.md

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

examples/cookbook/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/cookbook/assets/favicon.png

1.43 KB
Loading

examples/cookbook/assets/icon.png

21.9 KB
Loading

examples/cookbook/assets/splash.png

46.2 KB
Loading

examples/cookbook/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/cookbook/components/Home.tsx

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

0 commit comments

Comments
 (0)