-
Notifications
You must be signed in to change notification settings - Fork 274
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
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
2dd6ced
feature: basic example initial project
mdjastrzebski 27637ad
feature: basic RNTL setup
mdjastrzebski eca70de
feature: basic test
mdjastrzebski 23729f7
refactor: cleanup tests
mdjastrzebski 7c736ce
chore: extract jest.config.js and add jest-setup.js
mdjastrzebski 83ab5d1
docs: add readme
mdjastrzebski 51635da
refactor: styling, tweaks
mdjastrzebski 8e45434
refactor: fix lint issues
mdjastrzebski 1a3fcc5
refactor: migrat to expo
mdjastrzebski 6a9d053
chore: add testing library
mdjastrzebski a911b7a
feat: make it work
mdjastrzebski 6b57669
docs: tweak
mdjastrzebski 71299c6
Update examples/basic/App.tsx
mdjastrzebski cd7c5fb
chore: tweaks
mdjastrzebski deaae6d
refactor: improve tests
mdjastrzebski a3317db
chore: use repo prettier settings
mdjastrzebski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, | ||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
|
||
// 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(); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'], | ||
}; | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'], | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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 simplegetByText
would do the same.