Skip to content

feature: basic example app #1025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/basic/.expo-shared/assets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}
14 changes: 14 additions & 0 deletions examples/basic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/

# macOS
.DS_Store
20 changes: 20 additions & 0 deletions examples/basic/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import { SafeAreaView } from 'react-native';
import { LoginForm } from './components/LoginForm';
import { Home } from './components/Home';

const App = () => {
const [user, setUser] = React.useState<string | null>(null);

return (
<SafeAreaView>
{user == null ? (
<LoginForm onLoginSuccess={setUser} />
) : (
<Home user={user} />
)}
</SafeAreaView>
);
};

export default App;
9 changes: 9 additions & 0 deletions examples/basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Basic RNTL setup

This example is shows a basic modern React Native Testing Library setup in a template Expo app.

The app and related tests written in TypeScript, and it uses recommended practices like:

- testing large pieces of application instead of small components
- using `screen`-based queries
- using recommended query types, e.g. `byText`, `byLabelText`, `byPlaceholderText` over `byTestId`
110 changes: 110 additions & 0 deletions examples/basic/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import App from '../App';

/**
* A good place to start is having a tests that your component renders correctly.
*/
test('renders correctly', () => {
// Idiom: no need to capture render output, as we will use `screen` for queries.
render(<App />);

// Idiom: `getByXxx` is a predicate by itself, but we will use it with `expect().toBeTruthy()`
// to clarify our intent.
expect(screen.getByText('Sign in to Example App')).toBeTruthy();
});

/**
* Hint: It's best when your tests are similar to what a manual test scenarions would look like,
* i.e. a series of actions taken by the user, followed by a series of assertions verified from
* his point of view.
*/
test('User can sign in successully with correct credentials', async () => {
// Idiom: no need to capture render output, as we will use `screen` for queries.
render(<App />);

// Idiom: `getByXxx` is a predicate by itself, but we will use it with `expect().toBeTruthy()` to
// clarify our intent.
// Note: `.toBeTruthy()` is the preferred matcher for checking that elements are present.
expect(screen.getByText('Sign in to Example App')).toBeTruthy();
expect(screen.getByText('Username')).toBeTruthy();
expect(screen.getByText('Password')).toBeTruthy();

// Hint: we can use `getByLabelText` to find our text inputs in accessibility-friendly way.
fireEvent.changeText(screen.getByLabelText('Username'), 'admin');
fireEvent.changeText(screen.getByLabelText('Password'), 'admin1');

// Hint: we can use `getByText` to find our button by its text.
fireEvent.press(screen.getByText('Sign In'));

// Idiom: since pressing button triggers async operation we need to use `findBy` query to wait
// for the action to complete.
// Hint: subsequent queries do not need to use `findBy`, because they are used after the async action
// already finished
expect(await screen.findByText('Welcome admin!')).toBeTruthy();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to expect everything, right? This reads better as a scenario to me:

Suggested change
expect(await screen.findByText('Welcome admin!')).toBeTruthy();
await screen.findByText('Welcome admin!'));

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah thats a good question, I'used that in a similar pattern as we suggest using expect(getByText()).toBeTruthy() to clarify the intent, despite the fact that simple getByText would do the same.


// Idiom: use `queryByXxx` with `expect().toBeFalsy()` to assess that element is not present.
expect(screen.queryByText('Sign in to Example App')).toBeFalsy();
expect(screen.queryByText('Username')).toBeFalsy();
expect(screen.queryByText('Password')).toBeFalsy();
});

/**
* Another test case based on manual test scenario.
*
* Hint: Try to tests what a user would see and do, instead of assering internal component state
* that is not directly reflected in the UI.
*
* For this reason prefer quries that correspond to things directly observable by the user like:
* `getByText`, `getByLabelText`, `getByPlaceholderText, `getByDisplayValue`, `getByRole`, etc.
* over `getByTestId` which is not directly observable by the user.
*
* Note: that some times you will have to resort to `getByTestId`, but treat it as a last resort.
*/
test('User will see errors for incorrect credentials', async () => {
render(<App />);

expect(screen.getByText('Sign in to Example App')).toBeTruthy();
expect(screen.getByText('Username')).toBeTruthy();
expect(screen.getByText('Password')).toBeTruthy();

fireEvent.changeText(screen.getByLabelText('Username'), 'admin');
fireEvent.changeText(screen.getByLabelText('Password'), 'qwerty123');
fireEvent.press(screen.getByText('Sign In'));

// Hint: you can use custom Jest Native matcher to check text content.
expect(await screen.findByLabelText('Error')).toHaveTextContent(
'Incorrect username or password'
);

expect(screen.getByText('Sign in to Example App')).toBeTruthy();
expect(screen.getByText('Username')).toBeTruthy();
expect(screen.getByText('Password')).toBeTruthy();
});

/**
* Do not be afraid to write longer test scenarios, with repeating act and assert statements.
*/
test('User can sign in after incorrect attempt', async () => {
render(<App />);

expect(screen.getByText('Sign in to Example App')).toBeTruthy();
expect(screen.getByText('Username')).toBeTruthy();
expect(screen.getByText('Password')).toBeTruthy();

fireEvent.changeText(screen.getByLabelText('Username'), 'admin');
fireEvent.changeText(screen.getByLabelText('Password'), 'qwerty123');
fireEvent.press(screen.getByText('Sign In'));

expect(await screen.findByLabelText('Error')).toHaveTextContent(
'Incorrect username or password'
);

fireEvent.changeText(screen.getByLabelText('Password'), 'admin1');
fireEvent.press(screen.getByText('Sign In'));

expect(await screen.findByText('Welcome admin!')).toBeTruthy();
expect(screen.queryByText('Sign in to Example App')).toBeFalsy();
expect(screen.queryByText('Username')).toBeFalsy();
expect(screen.queryByText('Password')).toBeFalsy();
});
31 changes: 31 additions & 0 deletions examples/basic/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"expo": {
"name": "RNTL Example Basic",
"slug": "rntl-example-basic",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file added examples/basic/assets/adaptive-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/assets/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/assets/splash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions examples/basic/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};
26 changes: 26 additions & 0 deletions examples/basic/components/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';
import { StyleSheet, View, Text } from 'react-native';

type Props = {
user: string;
};

export function Home({ user }: Props) {
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome {user}!</Text>
</View>
);
}

const styles = StyleSheet.create({
container: {
padding: 20,
},
title: {
alignSelf: 'center',
fontSize: 24,
marginTop: 8,
marginBottom: 40,
},
});
131 changes: 131 additions & 0 deletions examples/basic/components/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as React from 'react';
import {
StyleSheet,
View,
Text,
TextInput,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';

type Props = {
onLoginSuccess: (user: string) => void;
};

export function LoginForm({ onLoginSuccess }: Props) {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const [error, setError] = React.useState<string | undefined>();
const [isLoading, setIsLoading] = React.useState(false);

const handleSignIn = async () => {
setIsLoading(true);

const user = await authUser(username, password);
setIsLoading(false);

if (user) {
setError(undefined);
onLoginSuccess(user);
} else {
setError('Incorrect username or password');
}
};

return (
<View style={styles.container}>
<Text style={styles.title}>Sign in to Example App</Text>

<Text style={styles.textLabel}>Username</Text>
<TextInput
value={username}
onChangeText={setUsername}
accessibilityLabel="Username"
autoCapitalize="none"
style={styles.textInput}
/>

<Text style={styles.textLabel}>Password</Text>
<TextInput
value={password}
onChangeText={setPassword}
accessibilityLabel="Password"
secureTextEntry={true}
style={styles.textInput}
/>

{error && (
<Text accessibilityLabel="Error" style={styles.validator}>
{error}
</Text>
)}

<TouchableOpacity onPress={handleSignIn} style={styles.button}>
{isLoading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.buttonText}>Sign In</Text>
)}
</TouchableOpacity>
</View>
);
}

/**
* Fake authentication function according to our abilities.
* @param username The username to authenticate.
* @param password The password to authenticate.
* @returns username if the username and password are correct, null otherwise.
*/
async function authUser(
username: string,
password: string
): Promise<string | null> {
return new Promise((resolve) =>
setTimeout(() => {
const hasValidCredentials = username === 'admin' && password === 'admin1';
resolve(hasValidCredentials ? username : null);
}, 250)
);
}

const styles = StyleSheet.create({
container: {
padding: 20,
},
title: {
alignSelf: 'center',
fontSize: 24,
marginTop: 8,
marginBottom: 40,
},
textLabel: {
fontSize: 16,
color: '#444',
},
textInput: {
fontSize: 20,
padding: 8,
marginVertical: 8,
borderColor: 'black',
borderWidth: 1,
},
button: {
backgroundColor: '#3256a8',
padding: 16,
alignItems: 'center',
justifyContent: 'center',
marginTop: 20,
minHeight: 56,
},
buttonText: {
fontSize: 20,
fontWeight: '600',
color: 'white',
},
validator: {
color: 'red',
fontSize: 18,
marginTop: 8,
},
});
7 changes: 7 additions & 0 deletions examples/basic/jest-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable no-undef, import/no-extraneous-dependencies */

// Import Jest Native matchers
import '@testing-library/jest-native/extend-expect';

// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
5 changes: 5 additions & 0 deletions examples/basic/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: '@testing-library/react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
setupFilesAfterEnv: ['./jest-setup.js'],
};
Loading