mirror of
https://github.com/callstack/react-native-testing-library.git
synced 2026-09-18 23:09:04 +08:00
docs: basic cookbook docs and app (#1622)
* docs: basic cookbook docs and app * chore: add tests * chore: enable example apps * chore: fix typecheck * chore: add lint & fix issues * docs: tweaks * docs: tweaks * docs: tweaks * chore: tweaks
This commit is contained in:
committed by
GitHub
parent
e418b6b5bf
commit
723c8e2c04
@@ -10,7 +10,7 @@ jobs:
|
||||
test-example:
|
||||
strategy:
|
||||
matrix:
|
||||
example: [basic]
|
||||
example: [basic, cookbook]
|
||||
|
||||
name: Test Example
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test-utils.*
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@callstack"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
|
||||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# General Node.js
|
||||
node_modules/
|
||||
.expo/
|
||||
dist/
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
|
||||
# Yarn 4.x
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const App = () => {
|
||||
return <View />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,8 @@
|
||||
# RNTL Cookbook
|
||||
|
||||
This example app gathers recipes from the [RNTL Cookbook](https://callstack.github.io/react-native-testing-library/cookbook).
|
||||
|
||||
Each recipe described in the Cookbook should have a corresponding code example in this repo.
|
||||
|
||||
Note:
|
||||
Since examples will showcase usage of different dependencies, the dependencies in `package.json` fill will grow much larger that in an normal React Native. This is fine 🐶☕️🔥.
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "RNTL Cookbook App",
|
||||
"slug": "rntl-cookbook",
|
||||
"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 not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,6 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { screen } from '@testing-library/react-native';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
import { WelcomeScreen } from './WelcomeScreen';
|
||||
|
||||
test('renders WelcomeScreen in light theme', () => {
|
||||
renderWithProviders(<WelcomeScreen />, { theme: 'light' });
|
||||
expect(screen.getByText('Theme: light')).toBeOnTheScreen();
|
||||
});
|
||||
|
||||
test('renders WelcomeScreen in dark theme', () => {
|
||||
renderWithProviders(<WelcomeScreen />, { theme: 'dark' });
|
||||
expect(screen.getByText('Theme: dark')).toBeOnTheScreen();
|
||||
});
|
||||
|
||||
test('renders WelcomeScreen with user', () => {
|
||||
renderWithProviders(<WelcomeScreen />, { user: { name: 'Jar-Jar' } });
|
||||
expect(screen.getByText(/hello Jar-Jar/i)).toBeOnTheScreen();
|
||||
});
|
||||
|
||||
test('renders WelcomeScreen without user', () => {
|
||||
renderWithProviders(<WelcomeScreen />, { user: null });
|
||||
expect(screen.getByText(/hello stranger/i)).toBeOnTheScreen();
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { useUser } from './providers/user-provider';
|
||||
import { useTheme } from './providers/theme-provider';
|
||||
|
||||
export function WelcomeScreen() {
|
||||
const theme = useTheme();
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>Hello {user ? user.name : 'Stranger'}</Text>
|
||||
<Text>Theme: {theme}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export type Theme = 'light' | 'dark';
|
||||
export const ThemeProvider = React.createContext<Theme | undefined>(undefined);
|
||||
|
||||
export function useTheme() {
|
||||
const theme = React.useContext(ThemeProvider);
|
||||
if (theme === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export type User = { name: string };
|
||||
export const UserProvider = React.createContext<User | null>(null);
|
||||
|
||||
export function useUser() {
|
||||
return React.useContext(UserProvider);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { render } from '@testing-library/react-native';
|
||||
import { User, UserProvider } from './providers/user-provider';
|
||||
import { Theme, ThemeProvider } from './providers/theme-provider';
|
||||
|
||||
interface RenderWithProvidersProps {
|
||||
user?: User | null;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
export function renderWithProviders<T>(
|
||||
ui: React.ReactElement<T>,
|
||||
options?: RenderWithProvidersProps,
|
||||
) {
|
||||
return render(
|
||||
<UserProvider.Provider value={options?.user ?? null}>
|
||||
<ThemeProvider.Provider value={options?.theme ?? 'light'}>{ui}</ThemeProvider.Provider>
|
||||
</UserProvider.Provider>,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/* eslint-disable no-undef, import/no-extraneous-dependencies */
|
||||
|
||||
// Import built-in Jest matchers
|
||||
import '@testing-library/react-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');
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: '@testing-library/react-native',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
||||
setupFilesAfterEnv: ['./jest-setup.ts'],
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject",
|
||||
"test": "jest",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^50.0.4",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.73.2",
|
||||
"react-native-web": "~0.19.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@testing-library/react-native": "^12.4.0",
|
||||
"@types/eslint": "^8.56.10",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "~18.2.45",
|
||||
"eslint": "^8.57.0",
|
||||
"jest": "^29.7.0",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.0.1"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"allowJs": false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,14 @@
|
||||
[
|
||||
{
|
||||
"text": "Docs",
|
||||
"link": "/docs/start/intro",
|
||||
"activeMatch": "^/docs/"
|
||||
},
|
||||
{
|
||||
"text": "Cookbook",
|
||||
"link": "/cookbook/",
|
||||
"activeMatch": "^/cookbook/"
|
||||
},
|
||||
{
|
||||
"text": "Examples",
|
||||
"link": "https://github.com/callstack/react-native-testing-library/tree/main/examples"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
["index", { "type": "dir", "name": "basics", "label": "Basic Recipes" }]
|
||||
@@ -0,0 +1 @@
|
||||
["custom-render"]
|
||||
@@ -0,0 +1,78 @@
|
||||
# Custom `render` function
|
||||
|
||||
### Summary
|
||||
|
||||
RNTL exposes the `render` function as the primary entry point for tests. If you make complex, repeating setups for your tests, consider creating a custom render function. The idea is to encapsulate common setup steps and test wiring inside a render function suitable for your tests.
|
||||
|
||||
### Example
|
||||
|
||||
```tsx title=test-utils.ts
|
||||
// ...
|
||||
|
||||
interface RenderWithProvidersProps {
|
||||
user?: User | null;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
export function renderWithProviders<T>(
|
||||
ui: React.ReactElement<T>,
|
||||
options?: RenderWithProvidersProps
|
||||
) {
|
||||
return render(
|
||||
<UserProvider.Provider value={options?.user ?? null}>
|
||||
<ThemeProvider.Provider value={options?.theme ?? 'light'}>{ui}</ThemeProvider.Provider>
|
||||
</UserProvider.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title=WelcomeScreen.test.tsx
|
||||
import { screen } from '@testing-library/react-native';
|
||||
import { renderWithProviders } from '../test-utils';
|
||||
// ...
|
||||
|
||||
test('renders WelcomeScreen with user', () => {
|
||||
renderWithProviders(<WelcomeScreen />, { user: { name: 'Jar-Jar' } });
|
||||
expect(screen.getByText(/hello Jar-Jar/i)).toBeOnTheScreen();
|
||||
});
|
||||
|
||||
test('renders WelcomeScreen without user', () => {
|
||||
renderWithProviders(<WelcomeScreen />, { user: null });
|
||||
expect(screen.getByText(/hello stranger/i)).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
Example [full source code](https://github.com/callstack/react-native-testing-library/tree/main/examples/cookbook/custom-render).
|
||||
|
||||
### More info
|
||||
|
||||
#### Additional params
|
||||
|
||||
A custom render function might accept additional parameters to allow for setting up different start conditions for a test, e.g., the initial state for global state management.
|
||||
|
||||
```tsx title=SomeScreen.test.tsx
|
||||
test('renders SomeScreen for logged in user', () => {
|
||||
renderScreen(<SomeScreen />, { state: loggedInState });
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
#### Multiple functions
|
||||
|
||||
Depending on the situation, you may declare more than one custom render function. For example, you have one function for testing application flows and a second for testing individual screens.
|
||||
|
||||
```tsx title=test-utils.tsx
|
||||
function renderNavigator(ui, options);
|
||||
function renderScreen(ui, options);
|
||||
```
|
||||
|
||||
#### Async function
|
||||
|
||||
Make it async if you want to put some async setup in your custom render function.
|
||||
|
||||
```tsx title=SomeScreen.test.tsx
|
||||
test('renders SomeScreen', async () => {
|
||||
await renderWithAsync(<SomeScreen />);
|
||||
// ...
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
# Introduction
|
||||
|
||||
This cookbook is intended to showcase best practices, tips & tricks, and ready-to-use recipes for using React Native Testing Library.
|
||||
|
||||
We invite you to contribute your favorite recipes to the Cookbook. More info [here](https://github.com/callstack/react-native-testing-library/issues/1624).
|
||||
Reference in New Issue
Block a user