Skip to content

Commit 2d434d7

Browse files
committed
Merge branch 'main' into poc/custom-renderer
# Conflicts: # package.json # src/__tests__/__snapshots__/render-debug.test.tsx.snap # yarn.lock
2 parents 1fb7552 + 0305877 commit 2d434d7

30 files changed

+2399
-1814
lines changed

.eslintignore

Lines changed: 3 additions & 0 deletions
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

Lines changed: 0 additions & 1 deletion
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 1 addition & 2 deletions
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
};

examples/cookbook/README.md

Lines changed: 23 additions & 3 deletions
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 git@github.com: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

Lines changed: 3 additions & 2 deletions
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
];
Lines changed: 52 additions & 0 deletions
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;
Lines changed: 40 additions & 0 deletions
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: ida.kristensen@example.com')).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+
});
Lines changed: 109 additions & 0 deletions
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+
email: 'ida.kristensen@example.com',
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+
email: 'elijah.ellis@example.com',
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+
email: 'miro.halko@example.com',
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+
};
Lines changed: 10 additions & 0 deletions
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+
};

0 commit comments

Comments
 (0)