Skip to content

Commit c7f9132

Browse files
committed
Merge branch 'main' into v13
# Conflicts: # package.json # src/__tests__/__snapshots__/render-debug.test.tsx.snap # yarn.lock
2 parents e6d06a7 + c56b223 commit c7f9132

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+3116
-3623
lines changed

.eslintignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
flow-typed/
22
build/
33
experiments-rtl/
4+
website/
5+
6+
jest-setup.ts

.eslintrc

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"extends": "@callstack",
33
"rules": {
4-
"flowtype/no-weak-types": 0,
54
"react-native/no-raw-text": 0,
65
"no-console": 1,
76
"react/no-multi-comp": 0,

CONTRIBUTING.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ Our pre-commit hooks verify that your commit message matches this format when co
3232

3333
### Linting and tests
3434

35-
We use `flow` for type checking, `eslint` with `prettier` for linting and formatting the code, and `jest` for testing. Our pre-commit hooks verify that the linter and tests pass when committing. You can also run the following commands manually:
35+
We use TypeScript for type checking, `eslint` with `prettier` for linting and formatting the code, and `jest` for testing. Our pre-commit hooks verify that the linter and tests pass when committing. You can also run the following commands manually:
3636

37-
- `yarn flow`: run flow on all files.
37+
- `yarn typecheck`: run TypeScript compiler on all files.
3838
- `yarn lint`: run eslint and prettier.
3939
- `yarn test`: run tests.
4040

@@ -43,7 +43,7 @@ We use `flow` for type checking, `eslint` with `prettier` for linting and format
4343
When you're sending a pull request:
4444

4545
- Prefer small pull requests focused on one change.
46-
- Verify that `flow`, `eslint` and tests are passing.
46+
- Verify that `typecheck`, `eslint` and tests are passing.
4747
- Preview the documentation to make sure it looks good.
4848
- Follow the pull request template when opening a pull request.
4949

babel.config.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ module.exports = {
1414
],
1515
env: {
1616
test: {
17-
// https://github.com/react-native-community/upgrade-support/issues/152
18-
plugins: ['@babel/plugin-transform-flow-strip-types'],
17+
presets: ['@react-native/babel-preset'],
1918
},
2019
},
2120
};
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react';
2+
import { Animated, ViewStyle } from 'react-native';
3+
4+
type AnimatedViewProps = {
5+
fadeInDuration?: number;
6+
style?: ViewStyle;
7+
children: React.ReactNode;
8+
useNativeDriver?: boolean;
9+
};
10+
11+
export function AnimatedView(props: AnimatedViewProps) {
12+
const fadeAnim = React.useRef(new Animated.Value(0)).current; // Initial value for opacity: 0
13+
14+
React.useEffect(() => {
15+
Animated.timing(fadeAnim, {
16+
toValue: 1,
17+
duration: props.fadeInDuration ?? 250,
18+
useNativeDriver: props.useNativeDriver ?? true,
19+
}).start();
20+
}, [fadeAnim]);
21+
22+
return (
23+
<Animated.View
24+
style={{
25+
...props.style,
26+
opacity: fadeAnim,
27+
}}
28+
>
29+
{props.children}
30+
</Animated.View>
31+
);
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { act, render, screen } from '@testing-library/react-native';
2+
import { AnimatedView } from '../AnimatedView';
3+
4+
describe('AnimatedView', () => {
5+
beforeEach(() => {
6+
jest.useFakeTimers();
7+
});
8+
9+
afterEach(() => {
10+
jest.useRealTimers();
11+
});
12+
13+
it('should use native driver when useNativeDriver is true', async () => {
14+
render(
15+
<AnimatedView fadeInDuration={250} useNativeDriver={true}>
16+
Test
17+
</AnimatedView>,
18+
);
19+
expect(screen.root).toHaveStyle({ opacity: 0 });
20+
21+
await act(() => jest.advanceTimersByTime(250));
22+
expect(screen.root).toHaveStyle({ opacity: 1 });
23+
});
24+
25+
it('should not use native driver when useNativeDriver is false', async () => {
26+
render(
27+
<AnimatedView fadeInDuration={250} useNativeDriver={false}>
28+
Test
29+
</AnimatedView>,
30+
);
31+
expect(screen.root).toHaveStyle({ opacity: 0 });
32+
33+
await act(() => jest.advanceTimersByTime(250));
34+
expect(screen.root).toHaveStyle({ opacity: 1 });
35+
});
36+
});

examples/cookbook/README.md

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
1-
# RNTL Cookbook
1+
<p align="center">
2+
<img alt="banner" src="assets/readme/banner.png" />
3+
</p>
24

3-
This example app gathers recipes from
4-
the [RNTL Cookbook](https://callstack.github.io/react-native-testing-library/cookbook).
5+
# React Native Testing Library Cookbook App
6+
Welcome to the React Native Testing Library (RNTL) Cookbook! This app is designed to provide developers with a collection of best practices, ready-made recipes, and tips & tricks to help you effectively test your React Native applications. Whether you’re just starting out with testing or looking to deepen your skills, this cookbook offers something for everyone.
57

68
Each recipe described in the Cookbook should have a corresponding code example screen in this repo.
79

810
Note:
911
Since examples will showcase usage of different dependencies, the dependencies in `package.json`
1012
file will grow much larger that in a normal React Native. This is fine 🐶☕️🔥.
13+
14+
## Running the App
15+
1. Clone the repo `git clone [email protected]:callstack/react-native-testing-library.git`
16+
2. Go to the `examples/cookbook` directory `cd examples/cookbook`
17+
3. Install dependencies `yarn`
18+
4. Run the app `yarn start`
19+
5. Run the app either on iOS or Android by clicking on `i` or `a` in the terminal.
20+
21+
## How to Contribute
22+
We invite all developers, from beginners to experts, to contribute your own recipes! If you have a clever solution, best practice, or useful tip, we encourage you to:
23+
24+
1. Submit a Pull Request with your recipe.
25+
2. Join the conversation on GitHub [here](https://github.com/callstack/react-native-testing-library/issues/1624) to discuss ideas, ask questions, or provide feedback.
26+
27+
## Screenshots From the App
28+
| Home Screen | Phonebook with Net. Req. Example |
29+
|-------------------------------------------------------|-----------------------------------------------------------------|
30+
| ![home-screenshot](assets/readme/home-screenshot.png) | ![phonebook-screenshot](assets/readme/phonebook-screenshot.png) |

examples/cookbook/app/index.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type Recipe = {
8282
};
8383

8484
const recipes: Recipe[] = [
85-
{ id: 2, title: 'Welcome Screen with Custom Render', path: 'custom-render/' },
86-
{ id: 1, title: 'Task List with Jotai', path: 'jotai/' },
85+
{ id: 1, title: 'Welcome Screen with Custom Render', path: 'custom-render/' },
86+
{ id: 2, title: 'Task List with Jotai', path: 'state-management/jotai/' },
87+
{ id: 3, title: 'Phone book with\na Variety of Net. Req. Methods', path: 'network-requests/' },
8788
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { Text } from 'react-native';
3+
import { User } from './types';
4+
import ContactsList from './components/ContactsList';
5+
import FavoritesList from './components/FavoritesList';
6+
import getAllContacts from './api/getAllContacts';
7+
import getAllFavorites from './api/getAllFavorites';
8+
9+
export default () => {
10+
const [usersData, setUsersData] = useState<User[]>([]);
11+
const [favoritesData, setFavoritesData] = useState<User[]>([]);
12+
const [error, setError] = useState<string | null>(null);
13+
14+
useEffect(() => {
15+
const _getAllContacts = async () => {
16+
const _data = await getAllContacts();
17+
setUsersData(_data);
18+
};
19+
const _getAllFavorites = async () => {
20+
const _data = await getAllFavorites();
21+
setFavoritesData(_data);
22+
};
23+
24+
const run = async () => {
25+
try {
26+
await Promise.all([_getAllContacts(), _getAllFavorites()]);
27+
} catch (e) {
28+
const message = isErrorWithMessage(e) ? e.message : 'Something went wrong';
29+
setError(message);
30+
}
31+
};
32+
33+
void run();
34+
}, []);
35+
36+
if (error) {
37+
return <Text>An error occurred: {error}</Text>;
38+
}
39+
40+
return (
41+
<>
42+
<FavoritesList users={favoritesData} />
43+
<ContactsList users={usersData} />
44+
</>
45+
);
46+
};
47+
48+
const isErrorWithMessage = (
49+
e: unknown,
50+
): e is {
51+
message: string;
52+
} => typeof e === 'object' && e !== null && 'message' in e;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
2+
import React from 'react';
3+
import PhoneBook from '../PhoneBook';
4+
import {
5+
mockServerFailureForGetAllContacts,
6+
mockServerFailureForGetAllFavorites,
7+
} from './test-utils';
8+
9+
jest.setTimeout(10000);
10+
11+
describe('PhoneBook', () => {
12+
it('fetches all contacts and favorites successfully and renders lists in sections correctly', async () => {
13+
render(<PhoneBook />);
14+
15+
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
16+
expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen();
17+
expect(await screen.findByText('Email: [email protected]')).toBeOnTheScreen();
18+
expect(await screen.findAllByText(/name/i)).toHaveLength(3);
19+
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen();
20+
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3);
21+
});
22+
23+
it('fails to fetch all contacts and renders error message', async () => {
24+
mockServerFailureForGetAllContacts();
25+
render(<PhoneBook />);
26+
27+
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
28+
expect(
29+
await screen.findByText(/an error occurred: error fetching contacts/i),
30+
).toBeOnTheScreen();
31+
});
32+
33+
it('fails to fetch favorites and renders error message', async () => {
34+
mockServerFailureForGetAllFavorites();
35+
render(<PhoneBook />);
36+
37+
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
38+
expect(await screen.findByText(/error fetching favorites/i)).toBeOnTheScreen();
39+
});
40+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { User } from '../types';
2+
import {http, HttpResponse} from "msw";
3+
import {setupServer} from "msw/node";
4+
5+
// Define request handlers and response resolvers for random user API.
6+
// By default, we always return the happy path response.
7+
const handlers = [
8+
http.get('https://randomuser.me/api/*', () => {
9+
return HttpResponse.json(DATA);
10+
}),
11+
];
12+
13+
export const server = setupServer(...handlers);
14+
15+
export const mockServerFailureForGetAllContacts = () => {
16+
server.use(
17+
http.get('https://randomuser.me/api/', ({ request }) => {
18+
// Construct a URL instance out of the intercepted request.
19+
const url = new URL(request.url);
20+
// Read the "results" URL query parameter using the "URLSearchParams" API.
21+
const resultsLength = url.searchParams.get('results');
22+
// Simulate a server error for the get all contacts request.
23+
// We check if the "results" query parameter is set to "25"
24+
// to know it's the correct request to mock, in our case get all contacts.
25+
if (resultsLength === '25') {
26+
return new HttpResponse(null, { status: 500 });
27+
}
28+
29+
return HttpResponse.json(DATA);
30+
}),
31+
);
32+
};
33+
34+
export const mockServerFailureForGetAllFavorites = () => {
35+
server.use(
36+
http.get('https://randomuser.me/api/', ({ request }) => {
37+
// Construct a URL instance out of the intercepted request.
38+
const url = new URL(request.url);
39+
// Read the "results" URL query parameter using the "URLSearchParams" API.
40+
const resultsLength = url.searchParams.get('results');
41+
// Simulate a server error for the get all favorites request.
42+
// We check if the "results" query parameter is set to "10"
43+
// to know it's the correct request to mock, in our case get all favorites.
44+
if (resultsLength === '10') {
45+
return new HttpResponse(null, { status: 500 });
46+
}
47+
48+
return HttpResponse.json(DATA);
49+
}),
50+
);
51+
};
52+
export const DATA: { results: User[] } = {
53+
results: [
54+
{
55+
name: {
56+
title: 'Mrs',
57+
first: 'Ida',
58+
last: 'Kristensen',
59+
},
60+
61+
id: {
62+
name: 'CPR',
63+
value: '250562-5730',
64+
},
65+
picture: {
66+
large: 'https://randomuser.me/api/portraits/women/26.jpg',
67+
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg',
68+
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg',
69+
},
70+
cell: '123-4567-890',
71+
},
72+
{
73+
name: {
74+
title: 'Mr',
75+
first: 'Elijah',
76+
last: 'Ellis',
77+
},
78+
79+
id: {
80+
name: 'TFN',
81+
value: '138117486',
82+
},
83+
picture: {
84+
large: 'https://randomuser.me/api/portraits/men/53.jpg',
85+
medium: 'https://randomuser.me/api/portraits/med/men/53.jpg',
86+
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/53.jpg',
87+
},
88+
cell: '123-4567-890',
89+
},
90+
{
91+
name: {
92+
title: 'Mr',
93+
first: 'Miro',
94+
last: 'Halko',
95+
},
96+
97+
id: {
98+
name: 'HETU',
99+
value: 'NaNNA945undefined',
100+
},
101+
picture: {
102+
large: 'https://randomuser.me/api/portraits/men/17.jpg',
103+
medium: 'https://randomuser.me/api/portraits/med/men/17.jpg',
104+
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/17.jpg',
105+
},
106+
cell: '123-4567-890',
107+
},
108+
],
109+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { User } from '../types';
2+
3+
export default async (): Promise<User[]> => {
4+
const res = await fetch('https://randomuser.me/api/?results=25');
5+
if (!res.ok) {
6+
throw new Error(`Error fetching contacts`);
7+
}
8+
const json = await res.json();
9+
return json.results;
10+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { User } from '../types';
2+
3+
export default async (): Promise<User[]> => {
4+
const res = await fetch('https://randomuser.me/api/?results=10');
5+
if (!res.ok) {
6+
throw new Error(`Error fetching favorites`);
7+
}
8+
const json = await res.json();
9+
return json.results;
10+
};

0 commit comments

Comments
 (0)